localStorage lets your web app save data directly in the browser — no server, no database, no expiry. It's one of the most useful browser APIs, and also one of the most frequently misused.
This guide covers everything: the API, object storage, storage events, size limits, security, and the common mistakes you'll encounter in real projects.
Quick-reference table
| Feature | localStorage |
sessionStorage |
|---|---|---|
| Lifetime | Until explicitly cleared | Until tab is closed |
| Scope | All tabs/windows on the same origin | Current tab only |
| Capacity | ~5–10 MB (per origin) | ~5–10 MB (per tab) |
| Accessible from JS | Yes | Yes |
| Sent to server | No | No |
| Available in service worker | No | No |
| Survives page reload | Yes | Yes |
| Survives browser close | Yes | No |
Basic operations
Both localStorage and sessionStorage share the same API. Every value must be a string.
// Set a value
localStorage.setItem('username', 'alice');
// Get a value (returns null if key doesn't exist)
const name = localStorage.getItem('username'); // 'alice'
// Remove a single key
localStorage.removeItem('username');
// Clear everything for this origin
localStorage.clear();
// Check how many keys are stored
console.log(localStorage.length); // 0
The same methods work for sessionStorage — just swap the name:
sessionStorage.setItem('draft', 'Hello world');
const draft = sessionStorage.getItem('draft');
Storing objects (JSON.stringify / JSON.parse)
localStorage only stores strings. To save objects or arrays, serialize them first:
const user = { id: 42, name: 'Alice', roles: ['admin', 'editor'] };
// Save
localStorage.setItem('user', JSON.stringify(user));
// Load
const stored = localStorage.getItem('user');
const parsed = stored ? JSON.parse(stored) : null;
console.log(parsed.roles); // ['admin', 'editor']
Helper functions to avoid repetition:
function lsSet(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function lsGet(key, fallback = null) {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return raw; // wasn't JSON — return as string
}
}
function lsRemove(key) {
localStorage.removeItem(key);
}
// Usage
lsSet('settings', { theme: 'dark', fontSize: 16 });
const settings = lsGet('settings', { theme: 'light', fontSize: 14 });
Iterating over all keys
// Option 1: index-based
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(key, value);
}
// Option 2: Object.entries (works because localStorage is array-like)
Object.entries(localStorage).forEach(([key, value]) => {
console.log(key, value);
});
Listening for storage changes
The storage event fires on other tabs in the same origin when localStorage changes. Useful for syncing state between tabs.
window.addEventListener('storage', (event) => {
console.log('Key changed:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('Storage object:', event.storageArea); // localStorage or sessionStorage
console.log('Origin:', event.url);
});
Note: The
storageevent does not fire in the tab that made the change — only in other tabs/windows with the same origin. To react in the same tab, use a custom event or a state management pattern.
Checking available space
There's no standard API to check remaining quota, but you can estimate it:
function estimateLocalStorageUsage() {
let total = 0;
for (const key of Object.keys(localStorage)) {
total += key.length + (localStorage.getItem(key) || '').length;
}
return `~${(total / 1024).toFixed(1)} KB used`;
}
When storage is full, setItem throws a QuotaExceededError:
try {
localStorage.setItem('key', veryLargeString);
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.warn('Storage full — consider clearing old data');
}
}
Common patterns
Theme preference
// Save
localStorage.setItem('theme', 'dark');
// Apply on load (before paint to avoid flash)
const theme = localStorage.getItem('theme') ?? 'light';
document.documentElement.setAttribute('data-theme', theme);
Form autosave (draft)
const input = document.querySelector('#essay');
// Restore draft
input.value = localStorage.getItem('essay-draft') ?? '';
// Save on every change (debounced)
input.addEventListener('input', debounce(() => {
localStorage.setItem('essay-draft', input.value);
}, 500));
// Clear on submit
document.querySelector('form').addEventListener('submit', () => {
localStorage.removeItem('essay-draft');
});
Namespaced keys (avoid collisions)
const APP_PREFIX = 'myapp_v1_';
function appSet(key, value) {
localStorage.setItem(APP_PREFIX + key, JSON.stringify(value));
}
function appGet(key, fallback = null) {
const raw = localStorage.getItem(APP_PREFIX + key);
return raw !== null ? JSON.parse(raw) : fallback;
}
Feature flag / onboarding
function hasSeenOnboarding() {
return localStorage.getItem('onboarding_done') === 'true';
}
function markOnboardingDone() {
localStorage.setItem('onboarding_done', 'true');
}
React hook: useLocalStorage
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item !== null ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const toStore = value instanceof Function ? value(stored) : value;
setStored(toStore);
try {
localStorage.setItem(key, JSON.stringify(toStore));
} catch (err) {
console.warn('localStorage write failed', err);
}
};
return [stored, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
Security considerations
localStorage is accessible by any JavaScript running on the same origin — including third-party scripts, browser extensions, and code injected via XSS.
Do NOT store:
- Passwords or password hashes
- Authentication tokens (prefer
HttpOnlycookies) - Credit card numbers or PII
- Session secrets
What's OK:
- UI preferences (theme, language, layout)
- Draft content (that isn't sensitive)
- Non-secret user settings
- Cache of public API responses
If you're storing JWT tokens, consider using short-lived tokens + refresh via
HttpOnlycookie instead.localStorageis vulnerable to XSS;HttpOnlycookies are not.
6 common mistakes
| Mistake | Problem | Fix |
|---|---|---|
localStorage.setItem('user', user) |
Stores "[object Object]" |
Use JSON.stringify(user) |
JSON.parse(localStorage.getItem('x')) |
Throws if getItem returns null |
Check for null first |
| Storing JWTs or passwords | XSS can read them | Use HttpOnly cookies for secrets |
No try/catch around setItem |
Throws QuotaExceededError silently |
Wrap in try/catch |
localStorage.getItem('count') + 1 |
String concatenation: "51" not 6 |
Parse: Number(localStorage.getItem('count')) + 1 |
Expecting storage event in the same tab |
Event only fires in other tabs | Use a state variable or custom event |
6 FAQ
Is localStorage available in all browsers?
Yes — all modern browsers and IE8+. In Node.js/SSR environments it doesn't exist; guard with typeof localStorage !== 'undefined' or use a package like localstorage-memory.
What's the storage limit?
Typically 5–10 MB per origin (varies by browser). Chrome and Firefox give ~5–10 MB; Safari gives ~5 MB. Large data (images, videos) should use IndexedDB instead.
Does localStorage work in incognito/private mode?
Yes, but data is cleared when the private session ends — behaving more like sessionStorage.
How is localStorage different from cookies?
Cookies are sent to the server with every request and can be HttpOnly (not accessible from JS). localStorage stays in the browser only, holds more data, and has a simpler API. Use cookies for authentication; use localStorage for client-side preferences.
Can different tabs share the same localStorage?
Yes — all tabs on the same origin (scheme + host + port) share the same localStorage. Changes in one tab fire the storage event in all others.
When should I use IndexedDB instead?
When you need to store more than ~5 MB, store binary data (Blobs, ArrayBuffers), or run complex queries. IndexedDB is asynchronous and far more powerful — but also more complex. For simple key-value preferences, localStorage is simpler and synchronous.