localStorage β Persistent Storage
localStorage stores data with no expiration time. Data persists across browser sessions, tabs (same origin), and restarts. The limit is approximately 5 MB per origin (varies by browser).
// setItem β store a value (strings only!)
localStorage.setItem('username', 'alice');
localStorage.setItem('theme', 'dark');
// getItem β retrieve (returns null if key doesn't exist)
const username = localStorage.getItem('username');
console.log(username); // "alice"
const missing = localStorage.getItem('nonexistent');
console.log(missing); // null
// removeItem β delete a single key
localStorage.removeItem('theme');
// clear β delete ALL keys in this origin (use with caution!)
// localStorage.clear();
// length β number of stored items
console.log(localStorage.length); // 1
// key(index) β get key name by index (order not guaranteed)
console.log(localStorage.key(0)); // "username"
// Direct property access (works but not recommended β conflicts with built-in props)
localStorage.color = 'blue'; // β avoid
localStorage.setItem('color', 'blue'); // β
preferred
username β "alice" missing β null length β 1 key(0) β "username"
sessionStorage β Tab-Scoped Storage
sessionStorage has the same API as localStorage but data is cleared when the tab (or window) is closed. Opening the same URL in a new tab creates a completely separate storage scope.
// Identical API to localStorage
sessionStorage.setItem('currentStep', '2');
sessionStorage.setItem('formDraft', JSON.stringify({ name: 'Bob', email: 'bob@mail.com' }));
const step = sessionStorage.getItem('currentStep');
console.log(step); // "2"
// Use cases:
// - Wizard/multi-step form progress (lose if user closes tab β by design)
// - Temporary shopping cart (before checkout)
// - One-time messages / alerts per session
// - Protecting against CSRF tokens (very short-lived)
sessionStorage.removeItem('currentStep');
// Cleared automatically when tab closes β no manual cleanup needed
Storing Objects with JSON
Web Storage only stores strings. Use JSON.stringify() to store objects and JSON.parse() to restore them:
// Store an object
const user = { name: 'Alice', role: 'admin', preferences: { theme: 'dark', lang: 'en' } };
localStorage.setItem('user', JSON.stringify(user));
// Retrieve and parse
const stored = localStorage.getItem('user');
const parsedUser = stored ? JSON.parse(stored) : null;
console.log(parsedUser.preferences.theme); // "dark"
// Store an array
const history = ['home', 'about', 'products'];
localStorage.setItem('navHistory', JSON.stringify(history));
const navHistory = JSON.parse(localStorage.getItem('navHistory') || '[]');
navHistory.push('contact');
localStorage.setItem('navHistory', JSON.stringify(navHistory));
// Helper wrapper to avoid repetition
const store = {
set(key, val) { localStorage.setItem(key, JSON.stringify(val)); },
get(key, fallback = null) {
const item = localStorage.getItem(key);
return item !== null ? JSON.parse(item) : fallback;
},
remove(key) { localStorage.removeItem(key); }
};
store.set('settings', { fontSize: 16, color: '#333' });
const settings = store.get('settings', { fontSize: 14, color: '#000' });
The storage Event β Cross-Tab Sync
When localStorage is changed in one tab, a storage event fires in all other tabs of the same origin β enabling real-time cross-tab communication:
// Listen for changes made in OTHER 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 area:', event.storageArea); // localStorage or sessionStorage
console.log('Origin URL:', event.url);
// Example: sync theme across tabs
if (event.key === 'theme') {
document.body.dataset.theme = event.newValue;
}
// Cross-tab logout
if (event.key === 'auth_token' && event.newValue === null) {
redirectToLogin();
}
});
// Note: the storage event does NOT fire in the tab that made the change
// It ONLY fires in other tabs/windows
Store the theme choice in localStorage and read it on page load before rendering to avoid flash of wrong theme (FOUC). Apply the theme class to <html> or <body> at the very top of <head> β before any CSS is applied.
IndexedDB β Large Structured Data
IndexedDB is a browser-native NoSQL database for large volumes of structured data. It supports transactions, indexes, cursors, and blob storage β but has a complex API. Libraries like Dexie.js make it practical:
// Raw IndexedDB API (simplified example)
const request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create an object store (like a table)
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
};
request.onsuccess = (event) => {
const db = event.target.result;
// Write a record
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').put({ id: 1, name: 'Alice', email: 'alice@example.com' });
tx.oncomplete = () => console.log('User saved');
};
// Using Dexie.js (much cleaner):
// const db = new Dexie('myDatabase');
// db.version(1).stores({ users: '++id, name, email' });
// await db.users.add({ name: 'Alice', email: 'alice@example.com' });
// const alice = await db.users.where('email').equals('alice@example.com').first();
Storage Options Comparison
| Feature | Cookies | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Capacity | ~4 KB | ~5 MB | ~5 MB | Hundreds of MB+ |
| Lifetime | Set by expiry / session | Permanent | Until tab closes | Permanent |
| Sent with requests | Yes (automatic) | No | No | No |
| Accessible from JS | Yes (unless HttpOnly) | Yes | Yes | Yes (async) |
| Cross-tab sharing | Yes | Yes (same origin) | No | Yes (same origin) |
| Data format | String | String | String | Any structured data |
| Use case | Auth, tracking | Preferences, cache | Form draft, wizard | Offline apps, large data |
localStorage and sessionStorage are accessible by any JavaScript on the page, including injected third-party scripts. Never store passwords, authentication tokens (JWTs), credit card numbers, or any personally identifiable information. For auth tokens, prefer HttpOnly cookies which are invisible to JavaScript.
Practical: Persisting Theme and Preferences
// Theme persistence
const themeBtn = document.getElementById('themeToggle');
// Apply saved theme on load
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
themeBtn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
// Persist font size preference
const fontSizeControl = document.getElementById('fontSize');
const savedSize = localStorage.getItem('fontSize') || '16';
document.body.style.fontSize = savedSize + 'px';
fontSizeControl.value = savedSize;
fontSizeControl.addEventListener('input', () => {
const size = fontSizeControl.value;
document.body.style.fontSize = size + 'px';
localStorage.setItem('fontSize', size);
});
ποΈ Practical Exercise
Build a persistent notes app:
- A
<textarea>for note input and a "Save" button. - On save, add the note to an array stored in localStorage under the key
'notes'. - On page load, read the array from localStorage and render all notes as cards.
- Each card has a "Delete" button that removes the note from the array and updates localStorage.
- Show the count of saved notes.
π₯ Challenge Exercise
Build a multi-tab counter using the storage event. Page A has increment/decrement buttons. Page B displays the current count. Any change on page A should instantly update page B via the storage event (without polling). Both pages should also persist the count across page refreshes. Test by opening both pages in separate tabs.
Summary
π Summary
localStorage: persistent, ~5 MB, same origin, string-only values.sessionStorage: same API, cleared when tab closes, not shared between tabs.- Use
JSON.stringify/parseto store and retrieve objects and arrays. - The
storageevent fires in other tabs when localStorage changes β useful for cross-tab sync. - IndexedDB handles large structured data; use Dexie.js to simplify its API.
- Never store passwords or auth tokens in Web Storage β use
HttpOnlycookies instead. - Always provide a fallback value when reading from storage in case the key doesn't exist.
Interview Questions
- What is the difference between localStorage and sessionStorage?
- Why can't you store objects directly in localStorage?
- How would you implement cross-tab communication using Web Storage?
- Why should you avoid storing JWT tokens in localStorage?
- When would you choose IndexedDB over localStorage?
Frequently Asked Questions
The browser throws a DOMException with the name QuotaExceededError. Wrap write operations in a try/catch to handle this gracefully: catch the error, inform the user, or remove old data to make room with localStorage.removeItem() before retrying.
No. Web Storage is scoped to the origin (scheme + domain + port). https://example.com cannot read https://other.com's storage, and http://example.com cannot read https://example.com's storage. However, all scripts running on the same origin can access the same storage β including ads and analytics scripts loaded on your page.
Yes. In private/incognito mode, localStorage is isolated to the private session and is cleared when the window closes β behaving like sessionStorage. On some browsers (older Safari), localStorage.setItem() may throw in private mode due to quota being set to zero. Always wrap writes in try/catch.