Step-by-step
-
1
Understand the API basics
localStoragestores everything as a string. To persist objects or arrays you must serialize them withJSON.stringifyon write and deserialize withJSON.parseon read. Values that cannot be serialized (functions,undefined, circular references) are silently dropped or converted.javascript// Write localStorage.setItem('theme', 'dark'); localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 30 })); // Read const theme = localStorage.getItem('theme'); // 'dark' or null const user = JSON.parse(localStorage.getItem('user')); // object or null // Delete one key localStorage.removeItem('theme'); // Wipe everything (careful in production) localStorage.clear(); -
2
Know when JSON.parse crashes
JSON.parse(null)returnsnullsafely —getItemreturnsnullfor missing keys, andnullcoerces to the string"null"which parses fine. ButJSON.parse(undefined)throws aSyntaxError, and so does parsing a corrupted string. Never read without guarding.javascript// Safe — null is valid input for JSON.parse JSON.parse(null); // → null JSON.parse('null'); // → null // These crash: JSON.parse(undefined); // SyntaxError: Unexpected token u JSON.parse(''); // SyntaxError: Unexpected end of JSON input JSON.parse('{bad json'); // SyntaxError -
3
Write a safe getItem helper
Wrap reads in
try/catchand provide a default value. This handles missing keys, corrupted data, and the rare case wherelocalStorageis unavailable (private browsing in some browsers, or security policies).javascriptfunction getItem(key, defaultValue = null) { try { const raw = localStorage.getItem(key); if (raw === null) return defaultValue; return JSON.parse(raw); } catch { return defaultValue; } } // Usage const prefs = getItem('user-prefs', { theme: 'light', lang: 'en' }); const count = getItem('visit-count', 0); -
4
Write a safe setItem helper
localStorage.setItemthrows aQuotaExceededErrorwhen the 5 MB limit is hit. Without a guard, this crashes your app silently. Wrap intry/catch— at minimum log the failure so you know it happened.javascriptfunction setItem(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (err) { if (err.name === 'QuotaExceededError') { console.warn('localStorage quota exceeded. Consider evicting old data.'); // Optional: remove the oldest entry and retry } else { console.error('localStorage write failed:', err); } } } // Usage setItem('user-prefs', { theme: 'dark', lang: 'ar' }); setItem('cart', [{ id: 1, qty: 2 }, { id: 5, qty: 1 }]); -
5
Respect the 5 MB size limit
The limit is per origin and shared across all keys. Large JSON blobs (images encoded as base64, entire API responses) exhaust it quickly. Store only what you need across sessions — IDs, preferences, short strings. For larger offline storage, use IndexedDB.
javascript// Rough size check before writing function estimatedSize(value) { return new Blob([JSON.stringify(value)]).size; // Bytes } // Check current total usage function storageUsed() { let total = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); total += (key.length + (localStorage.getItem(key) ?? '').length) * 2; // UTF-16 } return total; } console.log(`Storage used: ${(storageUsed() / 1024).toFixed(1)} KB`); -
6
Never store secrets or tokens
localStorageis accessible to any JavaScript running on the page — including third-party scripts, ads, and injected XSS. Never store authentication tokens, session cookies, passwords, API keys, or personal data there. UseHttpOnlycookies for auth tokens instead; the browser sends them automatically and JavaScript cannot read them. -
7
Listen for changes across tabs with the storage event
The
storageevent fires in every other open tab when a key is changed — not in the tab that made the change. Use it to sync state (theme, auth, cart) across tabs without polling.javascriptwindow.addEventListener('storage', (event) => { // event.key — the key that changed (null if clear() was called) // event.oldValue — previous value (string) // event.newValue — new value (string, or null if removed) // event.storageArea — the storage object that changed // event.url — URL of the page that made the change if (event.key === 'theme') { const theme = JSON.parse(event.newValue ?? 'null') ?? 'light'; document.documentElement.setAttribute('data-theme', theme); console.log(`Theme synced from another tab: ${theme}`); } if (event.key === null) { // localStorage.clear() was called — reset all state resetAppState(); } });
Tips & gotchas
- Prefix your keys with an app namespace (e.g., <code>myapp:theme</code>) to avoid collisions with third-party scripts or other projects on the same origin.
- <code>sessionStorage</code> has the same API but clears when the tab closes — use it for data that should not outlive the session.
- Always version your stored data schema. Add a <code>_version</code> field and migrate or clear on a version mismatch rather than crashing on unexpected shapes.
- In unit tests, mock <code>localStorage</code> with a simple object or the <code>jest-localstorage-mock</code> package — never rely on the real storage in tests.
- The <code>storage</code> event does not fire in the same tab that made the change — only in other tabs. Do not use it for same-tab synchronisation.
Wrapping up
localStorage is useful precisely because it is simple — but that simplicity hides a few real failure modes. Use the safe getItem/setItem helpers, keep the data small and non-sensitive, and you get a reliable client-side persistence layer with zero dependencies and no setup.