Browser Storage
Checking access...
Modern browsers provide several ways to store data on the client side. Each has different characteristics for persistence, size, and accessibility.
Storage Comparison
| Feature | localStorage | sessionStorage | Cookies | IndexedDB |
|---|---|---|---|---|
| Capacity | ~5-10 MB | ~5-10 MB | ~4 KB | Essentially unlimited |
| Persists? | ✅ Yes | ❌ Tab closes | Configurable | ✅ Yes |
| Sent to server? | ❌ No | ❌ No | ✅ Yes (every request) | ❌ No |
| Async? | ❌ No | ❌ No | ❌ No | ✅ Yes |
| Structured data? | Strings only | Strings only | Strings only | ✅ Full objects |
| Access | Any window | Same tab | Any window | Any window |
localStorage
Data persists indefinitely — survives browser closes, tab closes, and system restarts.
// SetlocalStorage.setItem("theme", "dark");localStorage.setItem("user", JSON.stringify({ name: "Alice", id: 1 }));
// Getconst theme = localStorage.getItem("theme"); // "dark"const user = JSON.parse(localStorage.getItem("user")); // { name: "Alice", id: 1 }
// RemovelocalStorage.removeItem("theme");
// Clear alllocalStorage.clear();
// Check sizeconsole.log(localStorage.length); // number of itemsCommon Patterns
Theme persistence:
// Load saved themeconst savedTheme = localStorage.getItem("theme") || "light";document.documentElement.setAttribute("data-theme", savedTheme);
// Toggle and savedocument.getElementById("theme-toggle").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);});Form draft auto-save:
const form = document.getElementById("blog-form");const draftKey = "blogDraft";
// Auto-save on inputform.addEventListener("input", () => { const formData = new FormData(form); const draft = Object.fromEntries(formData); localStorage.setItem(draftKey, JSON.stringify(draft));});
// Restore draft on page loadconst savedDraft = localStorage.getItem(draftKey);if (savedDraft) { const draft = JSON.parse(savedDraft); Object.entries(draft).forEach(([name, value]) => { const input = form.querySelector(`[name="${name}"]`); if (input) input.value = value; });}
// Clear draft on successful submitform.addEventListener("submit", () => { localStorage.removeItem(draftKey);});Caution
localStorage is synchronous and blocks the main thread. For small amounts of data (< 1 MB), this is fine. For larger data, use IndexedDB.
sessionStorage
Data persists only for the current tab session — cleared when the tab or browser closes.
// Same API as localStoragesessionStorage.setItem("scrollPosition", window.scrollY);sessionStorage.setItem("formStep", "3");
// Use case: restore scroll position after accidental navigationwindow.addEventListener("beforeunload", () => { sessionStorage.setItem("scrollPos", window.scrollY);});
window.addEventListener("load", () => { const savedPos = sessionStorage.getItem("scrollPos"); if (savedPos) { window.scrollTo(0, parseInt(savedPos)); }});Cookies
Cookies are sent with every HTTP request to the domain, making them useful for server-side session management.
// Set a cookie (expires in 7 days)document.cookie = `theme=dark; max-age=${7 * 24 * 60 * 60}; path=/; SameSite=Lax`;
// Set secure cookie (HTTPS only)document.cookie = "sessionId=abc123; Secure; HttpOnly; SameSite=Strict";
// Read all cookiesconsole.log(document.cookie); // "theme=dark; preference=compact"
// Parse cookiesfunction getCookie(name) { const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`)); return match ? decodeURIComponent(match[2]) : null;}
// Delete a cookie (set past expiration)document.cookie = "theme=; max-age=0; path=/";Cookie Attributes
| Attribute | Description |
|---|---|
max-age | Lifetime in seconds |
expires | Specific expiration date |
path | URL path the cookie applies to |
domain | Domain the cookie applies to |
Secure | HTTPS only |
HttpOnly | Not accessible from JavaScript (server-set only) |
SameSite | Strict, Lax, or None (CSRF protection) |
Danger
Never store sensitive data (passwords, tokens) in localStorage or sessionStorage — they are accessible to any JavaScript on the page. Use HttpOnly cookies for authentication tokens instead.
IndexedDB Overview
For large, structured data, IndexedDB is a full NoSQL database in the browser:
// Open/create a databaseconst request = indexedDB.open("MyApp", 1);
request.onupgradeneeded = (event) => { const db = event.target.result; // Create an object store (like a table) const store = db.createObjectStore("notes", { keyPath: "id", autoIncrement: true, }); store.createIndex("title", "title", { unique: false });};
request.onsuccess = (event) => { const db = event.target.result; console.log("Database ready");};
// Add a recordfunction addNote(note) { const db = request.result; const tx = db.transaction("notes", "readwrite"); const store = tx.objectStore("notes"); store.add(note);}
// Get all recordsfunction getAllNotes() { return new Promise((resolve, reject) => { const db = request.result; const tx = db.transaction("notes", "readonly"); const store = tx.objectStore("notes"); const all = store.getAll(); all.onsuccess = () => resolve(all.result); all.onerror = () => reject(all.error); });}For most use cases, use a wrapper library like Dexie.js or idb instead of raw IndexedDB.
Storage Limits and Errors
function safeLocalStorageSet(key, value) { try { localStorage.setItem(key, value); return true; } catch (error) { if (error.name === "QuotaExceededError") { console.warn("Storage is full — clearing old cache"); // Clear old items or notify user return false; } console.error("localStorage error:", error); return false; }}
// Check available storage (Chrome-based browsers)if (navigator.storage && navigator.storage.estimate) { navigator.storage.estimate().then(({ usage, quota }) => { console.log(`Using ${usage} of ${quota} bytes`); console.log(`Available: ${((quota - usage) / 1024 / 1024).toFixed(1)} MB`); });}Cache API (Service Workers)
For caching network responses, the Cache API is the modern approach:
// Check if Cache API is availableif ("caches" in window) { // Open a cache const cache = await caches.open("api-responses");
// Store a response cache.put("/api/users", new Response(JSON.stringify(users)));
// Read from cache const response = await cache.match("/api/users"); const data = await response.json();}Security Best Practices
- Never store secrets in localStorage — any XSS vulnerability exposes all data
- Use HttpOnly cookies for authentication tokens
- Sanitize data read from storage — never trust stored data
- Clean up old data — implement cache invalidation
- Handle storage errors — storage can be full or disabled (incognito mode)
// Check if storage is availablefunction isStorageAvailable(type) { try { const storage = window[type]; const key = "__test__"; storage.setItem(key, "1"); storage.removeItem(key); return true; } catch { return false; }}
if (!isStorageAvailable("localStorage")) { // Fall back to in-memory storage or cookies console.warn("localStorage not available");}Quick Reference
| API | Best For | Capacity |
|---|---|---|
localStorage | Preferences, drafts, cache | 5-10 MB |
sessionStorage | Tab-scoped data, form progress | 5-10 MB |
document.cookie | Server communication, auth tokens | 4 KB |
indexedDB | Large structured data, offline apps | GBs |
Cache API | Network response caching | Depends on disk |
Practice Exercises
Theme switcher with persistence: Build a dark/light mode toggle that saves the preference to localStorage and restores it on page load.
Session-aware form: Create a multi-step form that saves progress to sessionStorage. If the user accidentally closes the tab and reopens it, restore their progress (note: sessionStorage is cleared on close — use localStorage for true persistence, or simulate with a warning).
Offline-ready notes app: Build a simple notes app using localStorage. Users can add, edit, and delete notes. Each note has a title, body, and timestamp. Handle the “QuotaExceededError” gracefully.