localStorage and sessionStorage
Browser key-value storage that survives page reloads - what it stores, what it doesn't, and why auth tokens and PII must never go near it.
localStorage and sessionStorage are key-value stores built into every browser. They let you persist data across page loads without a server. Both accept only strings as keys and values, and both are scoped to the current origin (the combination of protocol, hostname, and port). The difference: localStorage persists until you clear it; sessionStorage is wiped when the tab closes. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.
The API is four methods: setItem to write, getItem to read, removeItem to delete one key, and clear to delete everything in that storage for the origin. Because both APIs only store strings, a number or boolean you write comes back as a string.
// Write
localStorage.setItem("theme", "dark");
localStorage.setItem("fontSize", "16");
// Read
console.log(localStorage.getItem("theme")); // "dark"
console.log(localStorage.getItem("fontSize")); // "16" (string, not number)
console.log(localStorage.getItem("missing")); // null
// Delete one key
localStorage.removeItem("fontSize");
// Delete everything for this origin
localStorage.clear();
// sessionStorage has the identical API but only lasts for the tab session
sessionStorage.setItem("draftTitle", "Untitled post");
console.log(sessionStorage.getItem("draftTitle")); // "Untitled post"
// Cleared automatically when the tab closesTo store objects or arrays, serialize them to a JSON string with JSON.stringify before writing, and deserialize with JSON.parse after reading. JSON.parse throws if the stored string is not valid JSON - which can happen if a previous version of your app wrote something else - so always wrap it in try/catch.
const settings = { theme: "dark", fontSize: 16, compact: true };
// Write: serialize to JSON first
localStorage.setItem("settings", JSON.stringify(settings));
// Read: parse back to an object
function loadSettings() {
try {
const raw = localStorage.getItem("settings");
return raw ? JSON.parse(raw) : null;
} catch {
// The stored string was not valid JSON - fall back to a default
return null;
}
}
const loaded = loadSettings();
console.log(loaded?.theme); // "dark"
console.log(typeof loaded?.fontSize); // "number" (JSON.parse restores the type)The storage event fires on all other tabs and windows that share the same origin whenever localStorage changes. This lets separate tabs stay in sync without a server connection. The event does not fire in the tab that made the change.
// In Tab A: listen for changes made by other tabs
window.addEventListener("storage", (event) => {
console.log("key changed:", event.key);
console.log("old value:", event.oldValue);
console.log("new value:", event.newValue);
if (event.key === "theme") {
document.body.dataset.theme = event.newValue ?? "light";
}
});
// In Tab B: making this change fires the event in Tab A (not in Tab B itself)
localStorage.setItem("theme", "light");In production
Both APIs are synchronous and run on the main thread - large reads and writes show up as long tasks in a performance profile. Quota is around 5 MB per origin; exceeding it throws a QuotaExceededError. Any JavaScript on the page can read everything in localStorage, so never store auth tokens, session cookies, or personally identifiable information here. Use HttpOnly cookies for secrets - the browser keeps them out of JavaScript entirely.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free