How to Safely Work with Local Storage and Session Storage
localStorage seems so simple. setItem, getItem, done. Then you ship to production and discover all the ways it can break.
I've seen apps crash because localStorage was full. I've seen security incidents from storing auth tokens in plain text. I've seen white screens because JSON.parse choked on corrupted data.
Let me save you some pain.
localStorage vs sessionStorage
Quick refresher:
- localStorage: Persists forever (until cleared). Survives browser restarts.
- sessionStorage: Dies when the tab closes.
Same API, different lifetimes. Everything below applies to both.
Problem 1: It Only Stores Strings
Try this:
localStorage.setItem('user', { name: 'John', age: 30 });
console.log(localStorage.getItem('user'));
// "[object Object]" 🤦
You have to serialize objects yourself:
// Save
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
// Load
const user = JSON.parse(localStorage.getItem('user'));
But here's the trap: JSON.parse(null) returns null (fine), but JSON.parse("undefined") or JSON.parse("corrupt data") throws an exception. If you don't catch it, your app crashes.
// ❌ This will crash on bad data
const settings = JSON.parse(localStorage.getItem('settings'));
// âś… Safe version
function getJSON(key, fallback = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : fallback;
} catch {
return fallback;
}
}
const settings = getJSON('settings', { theme: 'light' });
Problem 2: Storage Can Be Full
localStorage has a limit—usually 5-10MB depending on the browser. When you hit it, setItem throws a QuotaExceededError.
function safeSetItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.warn('localStorage is full');
// Maybe clear old data, or warn the user
}
return false;
}
}
I've seen apps that cached API responses in localStorage without any eviction strategy. Works great for months, then suddenly starts failing for long-time users.
Problem 3: Security
Never store sensitive data in localStorage. Any JavaScript on your page can read it—including that third-party analytics script or any XSS attack.
What NOT to store:
- Auth tokens (use httpOnly cookies instead)
- Passwords (obviously)
- Credit card info
- Personal data you wouldn't want leaked
What's fine to store:
- UI preferences (theme, language)
- Non-sensitive cache data
- Draft content (with user awareness)
Building a Storage Wrapper
After getting burned a few times, I started using a wrapper that handles all these edge cases:
const storage = {
get(key, fallback = null) {
try {
const item = localStorage.getItem(key);
if (item === null) return fallback;
return JSON.parse(item);
} catch {
return fallback;
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
console.warn('Storage failed:', e);
return false;
}
},
remove(key) {
localStorage.removeItem(key);
},
// Set with expiration
setWithExpiry(key, value, ttlMs) {
const item = {
value,
expiry: Date.now() + ttlMs
};
this.set(key, item);
},
getWithExpiry(key, fallback = null) {
const item = this.get(key);
if (!item) return fallback;
if (Date.now() > item.expiry) {
this.remove(key);
return fallback;
}
return item.value;
}
};
// Usage
storage.set('preferences', { theme: 'dark', fontSize: 16 });
const prefs = storage.get('preferences', { theme: 'light', fontSize: 14 });
// Cache API response for 1 hour
storage.setWithExpiry('userProfile', userData, 60 * 60 * 1000);
const cached = storage.getWithExpiry('userProfile');
Storage Might Not Be Available
Private browsing mode in some browsers disables localStorage entirely. Some users disable it manually. Some corporate browsers block it.
function isStorageAvailable() {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
}
if (!isStorageAvailable()) {
// Fall back to in-memory storage or warn user
}
Performance Note
localStorage is synchronous and blocks the main thread. For small data, not a problem. But if you're storing large objects or reading frequently, it can cause jank.
For larger data or more complex needs, consider IndexedDB (async, handles more data). But for simple preferences and small caches, localStorage is fine—just wrap it safely.
Quick Rules
- Always wrap
JSON.parsein try/catch - Always wrap
setItemfor quota errors - Never store auth tokens or sensitive data
- Consider expiration for cached data
- Check if storage is available before relying on it
localStorage is convenient, but it's not as simple as it looks. A small wrapper saves a lot of headaches.