Web Storage: localStorage and sessionStorage
Web Storage gives every browser tab two key/value stores accessible from JavaScript: localStorage (persists across reloads and tab restarts) and sessionStorage (lives only as long as the tab is open). Both are synchronous, string-only, and scoped to the origin — the protocol + host + port combination. They are simple, fast and ubiquitous; just don't reach for them when you have a lot of data.
The five methods
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");
localStorage.clear(); // wipe the whole store
localStorage.length; // number of keys
localStorage.key(0); // the first key (insertion order is not guaranteed)sessionStorage has exactly the same API — different lifetime.
localStorage vs sessionStorage
localStorage— persists indefinitely until cleared by user, by your code, or by browser eviction. Shared between tabs of the same origin.sessionStorage— wiped when the tab is closed. Not shared between tabs, even of the same origin.Both are per-origin: a script on
https://app.example.comcannot read storage fromhttps://api.example.com.Both are synchronous and block the main thread while reading/writing.
It is strings all the way down
The store can only hold strings. Anything else is coerced via String(value), which loses structure.
localStorage.setItem("count", 7);
console.log(localStorage.getItem("count")); // "7" ← string, not number
console.log(typeof localStorage.getItem("count")); // "string"
localStorage.setItem("user", { name: "Ada" });
console.log(localStorage.getItem("user")); // "[object Object]" ← junk7 string [object Object]
JSON round-trip for objects
storage-helpers.js
function save(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function load(key, fallback = null) {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
save("user", { name: "Ada", roles: ["admin"] });
load("user"); // { name: "Ada", roles: ["admin"] }
load("missing", { name: "Guest" }); // { name: "Guest" }Capacity and quota
The spec only requires "at least 5 MB", and browsers vary:
Chromium browsers: roughly 5–10 MB per origin.
Firefox: 10 MB per origin (often shared across eTLD+1).
Safari: 5 MB per origin; cleared after ~7 days of inactivity by ITP.
Mobile Safari and in-app webviews are the most aggressive about evicting storage.
Handle quota errors
function tryWrite(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (err) {
if (err.name === "QuotaExceededError") {
console.warn("localStorage is full");
return false;
}
throw err;
}
}The storage event: syncing across tabs
When localStorage changes in one tab, every other tab of the same origin receives a storage event. The originating tab does not. This is the easiest way to keep multiple tabs in sync.
addEventListener("storage", (e) => {
if (e.key === "theme") {
document.documentElement.dataset.theme = e.newValue;
}
// e.key, e.oldValue, e.newValue, e.url, e.storageArea
});
// In another tab:
localStorage.setItem("theme", "dark");sessionStorage does not fire storage events between tabs (because each tab has its own session store).
What not to store
Auth tokens or anything secret. Any script on the page — including a compromised npm dependency — can read
localStorage. Prefer HTTP-only cookies for session tokens.Large binary data. Anything bigger than a few hundred KB belongs in IndexedDB.
Data you need on the server. Storage stays in the browser; the server never sees it.
Sensitive user data. It survives across sessions and may be backed up by the OS.
A typical real-world use
Remember user preferences
const Theme = {
get() {
return localStorage.getItem("theme") || "system";
},
set(value) {
localStorage.setItem("theme", value);
document.documentElement.dataset.theme = value;
},
};
// On boot:
document.documentElement.dataset.theme = Theme.get();
// On click:
document.querySelector("#dark").addEventListener("click", () => {
Theme.set("dark");
});