Simple key-value storage in the browser
The Web Storage API gives every origin two synchronous string key-value stores —
localStorage and sessionStorage — sharing the same Storage interface but
differing in lifetime and scope. This reference lists the methods and the length
property, the StorageEvent fired on cross-tab changes, and the quota limits that
trip up real apps.
How it works
Both areas expose the same handful of methods, and all values are strings:
localStorage.setItem("theme", JSON.stringify({ dark: true }));
const raw = localStorage.getItem("theme");
let theme = { dark: false };
try {
const parsed = raw ? JSON.parse(raw) : null;
if (parsed && typeof parsed.dark === "boolean") theme = parsed;
} catch {
/* corrupt value — fall back to default */
}
window.addEventListener("storage", (e) => {
if (e.key === "theme") applyTheme(e.newValue);
});
Writes are synchronous and can block the main thread, so keep payloads small.
When you exceed the per-origin quota the call throws QuotaExceededError. The
storage event lets other tabs of the same origin react to changes, but it never
fires in the tab that wrote the value.
localStorage vs sessionStorage: which to use
The right choice depends on how long the data needs to live and whether it should be shared across tabs:
| Property | localStorage | sessionStorage |
|---|---|---|
| Lifetime | Until explicitly cleared | Until the tab closes |
| Shared across tabs? | Yes — all same-origin tabs | No — scoped to single tab |
| Persists across restarts? | Yes | No |
| Cleared on logout? | Only if you call removeItem/clear | Automatically on tab close |
| Good for | User preferences, cached data | Wizard state, one-time tokens |
A common mistake is using localStorage for per-session data (like a shopping cart mid-checkout), which then persists unexpectedly across browser restarts and across multiple tabs. Conversely, using sessionStorage for preferences that should follow the user means they reset every time a tab is opened.
Common patterns and pitfalls
Always validate the shape after parsing. Stored values can be corrupted by older app versions, browser extensions, or manual devtools edits. A JSON.parse that succeeds does not guarantee the object has the fields your code expects:
function loadPrefs() {
try {
const raw = localStorage.getItem("prefs");
const parsed = raw ? JSON.parse(raw) : null;
// validate shape before trusting
if (parsed && typeof parsed.theme === "string" && typeof parsed.lang === "string") {
return parsed;
}
} catch {
// corrupt JSON — fall through to defaults
}
return { theme: "light", lang: "en" };
}
Wrap writes in try/catch. Two common situations throw on write: the origin has hit its quota, and the browser is in a private/incognito mode that enforces stricter or zero-quota storage. Production code that writes to localStorage without wrapping will crash in these scenarios.
The storage event does not fire in the writing tab. If you want a tab to react to its own writes, call your update logic directly after the write rather than relying on the event.
Enumerating keys is order-indeterminate. Iterating with length and key(i) works, but the order of keys is not guaranteed to be insertion order. If you need ordered keys, maintain a separate index array or use a different storage mechanism.
Security limits
Web Storage is same-origin but not same-application. Any JavaScript running on your origin — including third-party scripts from CDNs, analytics providers, and ad networks — can read and write your localStorage. Never store authentication tokens, session identifiers, or personal data in Web Storage; use httpOnly secure cookies for sensitive session state.
Tips and notes
- Store strings only; serialise with
JSON.stringifyand validate the shape afterJSON.parse. - Wrap writes in
try/catch— private-browsing modes and full quotas both throw. lengthandkey(i)let you enumerate entries, but order is not guaranteed across browsers.- Use
sessionStoragefor per-tab, throwaway state andlocalStoragefor data that should survive a reload.