How to Store JavaScript Objects in HTML5 localStorage/sessionStorage
I’m trying to store a JavaScript object in HTML5 localStorage, but my object is being converted to a string. I can store and retrieve primitive JavaScript types and arrays using localStorage, but objects don’t seem to work as expected.
Here’s my code:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
console.log('typeof testObject: ' + typeof testObject);
console.log('testObject properties:');
for (var prop in testObject) {
console.log(' ' + prop + ': ' + testObject[prop]);
}
// Put the object into storage
localStorage.setItem('testObject', testObject);
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('typeof retrievedObject: ' + typeof retrievedObject);
console.log('Value of retrievedObject: ' + retrievedObject);
The console output shows:
typeof testObject: object
testObject properties:
one: 1
two: 2
three: 3
typeof retrievedObject: string
Value of retrievedObject: [object Object]
It appears that the setItem method is converting the input to a string before storing it. I’ve observed this behavior in Safari, Chrome, and Firefox, so I assume it’s related to the HTML5 Web Storage specification rather than a browser-specific issue.
I’ve tried to understand the structured clone algorithm described in the specification, but I’m not sure if my problem is related to my object’s properties not being enumerable.
Is there an easy workaround to store JavaScript objects in localStorage or sessionStorage while preserving their structure and functionality?
You need to use JSON.stringify() to convert your JavaScript object to a string before storing it, and JSON.parse() to convert it back to an object when retrieving it from localStorage or sessionStorage. This approach preserves the object structure and allows you to store complex data types in the browser’s storage APIs.
Contents
- Understanding localStorage Limitations
- JSON.stringify() and JSON.parse() Solution
- Alternative Storage Approaches
- Error Handling and Edge Cases
- Performance Considerations
- Security Considerations
- Best Practices
Understanding localStorage Limitations
The HTML5 Web Storage specification, which includes both localStorage and sessionStorage, has a fundamental limitation: it can only store string values. When you call localStorage.setItem(key, value), the value is automatically converted to a string using the toString() method. This is why your JavaScript object becomes the string “[object Object]” when stored.
According to the MDN Web Docs, the Web Storage API stores data as key-value pairs, where both keys and values are stored as strings. This means that objects, arrays, and other complex data structures must be serialized before storage and deserialized after retrieval.
The specification also mentions that the storage quota varies between browsers, with most modern browsers offering around 5-10MB of storage space for localStorage, while sessionStorage typically has the same limits but is cleared when the page session ends.
JSON.stringify() and JSON.parse() Solution
The most common and reliable approach to storing JavaScript objects in localStorage is to use JSON serialization and deserialization.
Basic Implementation
Here’s how you should modify your code:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
// Put the object into storage as a JSON string
localStorage.setItem('testObject', JSON.stringify(testObject));
// Retrieve the object from storage and parse it back to an object
var retrievedObject = JSON.parse(localStorage.getItem('testObject'));
console.log('typeof retrievedObject: ' + typeof retrievedObject);
console.log('retrievedObject:');
for (var prop in retrievedObject) {
console.log(' ' + prop + ': ' + retrievedObject[prop]);
}
This will correctly preserve your object structure and maintain the proper type information.
Helper Functions
For more convenient usage, you can create helper functions:
// Store an object in localStorage
function storeObject(key, object) {
try {
localStorage.setItem(key, JSON.stringify(object));
return true;
} catch (error) {
console.error('Failed to store object:', error);
return false;
}
}
// Retrieve an object from localStorage
function getObject(key) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('Failed to retrieve object:', error);
return null;
}
}
// Usage
storeObject('testObject', { 'one': 1, 'two': 2, 'three': 3 });
var retrieved = getObject('testObject');
As W3C Web Storage specification explains, this approach works because JSON (JavaScript Object Notation) is a lightweight data interchange format that’s natively supported by JavaScript browsers.
Alternative Storage Approaches
While JSON.stringify() and JSON.parse() are the most common solutions, there are alternative approaches depending on your specific needs.
Custom Serialization
For objects that contain non-serializable data (like functions, Date objects, or undefined values), you can implement custom serialization:
function customSerialize(obj) {
return JSON.stringify(obj, function(key, value) {
// Handle Date objects
if (value instanceof Date) {
return { '__type__': 'Date', '__value__': value.getTime() };
}
// Handle undefined values
if (value === undefined) {
return { '__type__': 'undefined' };
}
return value;
});
}
function customDeserialize(str) {
return JSON.parse(str, function(key, value) {
if (value && value.__type__ === 'Date') {
return new Date(value.__value__);
}
if (value && value.__type__ === 'undefined') {
return undefined;
}
return value;
});
}
IndexedDB
For larger datasets or more complex data structures, consider using IndexedDB, which is a NoSQL database built into modern browsers:
// Open or create database
const request = indexedDB.open('MyDatabase', 1);
request.onerror = function(event) {
console.error('Database error:', event.target.error);
};
request.onsuccess = function(event) {
const db = event.target.result;
// Store complex objects directly
const transaction = db.transaction(['objects'], 'readwrite');
const objectStore = transaction.objectStore('objects');
objectStore.add(testObject, 'testObject');
};
Base64 Encoding
For binary data or when you need to avoid JSON parsing overhead:
// Store as Base64
const base64String = btoa(JSON.stringify(testObject));
localStorage.setItem('testObject', base64String);
// Retrieve from Base64
const retrievedBase64 = localStorage.getItem('testObject');
const retrievedObject = JSON.parse(atob(retrievedBase64));
Error Handling and Edge Cases
When working with localStorage and JSON serialization, you need to handle several potential issues:
Circular References
Objects with circular references cannot be serialized with JSON.stringify():
const obj = { name: 'Test' };
obj.self = obj; // Circular reference
try {
localStorage.setItem('circular', JSON.stringify(obj));
} catch (error) {
console.error('Circular reference detected:', error);
// Handle circular reference
}
As JavaScript.info explains, you need to detect and handle circular references before serialization.
Storage Quota Exceeded
Both localStorage and sessionStorage have limited storage space (typically 5-10MB):
function safeStorage(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
if (error.name === 'QuotaExceededError') {
// Handle quota exceeded
console.error('Storage quota exceeded');
// Clear old data or use alternative storage
} else {
throw error;
}
}
}
Invalid JSON
Handle cases where stored data might be corrupted or invalid:
function safeGetObject(key) {
try {
const item = localStorage.getItem(key);
if (!item) return null;
return JSON.parse(item);
} catch (error) {
console.error('Invalid JSON in storage:', error);
localStorage.removeItem(key); // Remove corrupted data
return null;
}
}
Performance Considerations
When storing large objects or frequently accessing localStorage, consider these performance implications:
Object Size
Large objects impact both storage space and performance:
// Estimate storage size
function getStorageSize(obj) {
return JSON.stringify(obj).length;
}
// Check available storage
function getUsedStorage() {
let total = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
total += localStorage.getItem(key).length;
}
return total;
}
Batch Operations
Minimize localStorage access with batch operations:
// Store multiple objects in a single operation
function batchStore(objects) {
const batch = {};
for (const [key, value] of Object.entries(objects)) {
batch[key] = JSON.stringify(value);
}
localStorage.setItem('batch', JSON.stringify(batch));
}
// Retrieve multiple objects
function batchStoreRetrieve(keys) {
const batch = JSON.parse(localStorage.getItem('batch') || '{}');
const result = {};
for (const key of keys) {
if (batch[key]) {
result[key] = JSON.parse(batch[key]);
}
}
return result;
}
Compression
For large datasets, consider compression:
// Simple compression using encodeURI/decodeURI
function compressObject(obj) {
return encodeURI(JSON.stringify(obj));
}
function decompressObject(compressed) {
return JSON.parse(decodeURI(compressed));
}
Security Considerations
When storing data in localStorage, keep these security considerations in mind:
Sensitive Data
Never store sensitive information in localStorage, as it’s accessible to any script on the same domain:
// Check for sensitive data before storage
function isSafeForStorage(obj) {
const sensitiveKeys = ['password', 'token', 'ssn', 'creditCard'];
return !sensitiveKeys.some(key => key in obj);
}
if (!isSafeForStorage(testObject)) {
throw new Error('Object contains sensitive data');
}
XSS Protection
As OWASP Web Storage Security warns, localStorage is vulnerable to XSS attacks. Sanitize data and implement Content Security Policy (CSP) headers.
Data Tampering
Implement data integrity checks:
function storeWithChecksum(key, obj) {
const data = JSON.stringify(obj);
const checksum = btoa(data); // Simple checksum
localStorage.setItem(key, JSON.stringify({
data,
checksum
}));
}
function retrieveWithChecksum(key) {
const stored = JSON.parse(localStorage.getItem(key));
const calculatedChecksum = btoa(stored.data);
if (stored.checksum !== calculatedChecksum) {
throw new Error('Data tampering detected');
}
return JSON.parse(stored.data);
}
Best Practices
Follow these best practices for working with localStorage objects:
Namespace Your Keys
Avoid key collisions by using a naming convention:
const storage = {
user: {
set: (key, value) => localStorage.setItem(`user_${key}`, JSON.stringify(value)),
get: (key) => JSON.parse(localStorage.getItem(`user_${key}`)),
remove: (key) => localStorage.removeItem(`user_${key}`)
}
};
Implement Expiration
Add expiration to stored data:
function storeWithExpiration(key, obj, expirationHours) {
const expiration = Date.now() + (expirationHours * 60 * 60 * 1000);
localStorage.setItem(key, JSON.stringify({
data: obj,
expiration
}));
}
function getWithExpiration(key) {
const stored = JSON.parse(localStorage.getItem(key));
if (!stored) return null;
if (Date.now() > stored.expiration) {
localStorage.removeItem(key);
return null;
}
return stored.data;
}
Fallback Mechanisms
Provide fallback when localStorage is unavailable:
function isLocalStorageAvailable() {
try {
const testKey = '__test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
}
function safeLocalStorageGet(key) {
if (!isLocalStorageAvailable()) {
console.warn('localStorage not available');
return null;
}
return JSON.parse(localStorage.getItem(key));
}
Migration Strategy
Handle schema changes in stored objects:
function migrateObject(obj, version = 1) {
// Implement migration logic based on version
switch (version) {
case 1:
// Convert from v1 to v2
return migrateV1ToV2(obj);
case 2:
// Current version
return obj;
default:
throw new Error('Unknown version');
}
}
function storeWithVersion(key, obj, version) {
localStorage.setItem(key, JSON.stringify({
data: obj,
version
}));
}