localStorage & sessionStorage
The Web Storage API gives every page a simple key-value store that lives in the browser, no server round-trip required. It comes in two flavors — localStorage, which persists until explicitly cleared, and sessionStorage, which lasts only for the lifetime of a tab. Both share the same tiny API, store strings only, and are perfect for remembering user preferences, caching small payloads, or holding draft form data. Understanding their limits and quirks keeps you from reaching for cookies or a database when a few lines of storage will do.
The core API
Both localStorage and sessionStorage implement the same Storage interface, exposed as global objects on window. You read and write through five methods plus a length property.
| Member | Description |
|---|---|
setItem(key, value) | Stores value under key. Both are coerced to strings. |
getItem(key) | Returns the string for key, or null if it doesn’t exist. |
removeItem(key) | Deletes the entry for key. |
clear() | Removes every entry in that origin’s store. |
key(index) | Returns the key name at the given numeric index. |
length | The number of stored entries (a property, not a method). |
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");
console.log(theme); // "dark"
console.log(localStorage.getItem("missing")); // null
localStorage.removeItem("theme");
console.log(localStorage.length); // 0
Output:
dark
null
0
Missing keys return
null, neverundefined. Always handle thenullcase before parsing or rendering a stored value.
localStorage vs sessionStorage
The two stores behave identically at the API level; the difference is lifetime and scope. localStorage survives page reloads, tab closes, and browser restarts. sessionStorage is scoped to a single tab and is wiped the moment that tab closes — opening the same site in a new tab starts with an empty sessionStorage.
| Aspect | localStorage | sessionStorage |
|---|---|---|
| Lifetime | Until explicitly cleared | Until the tab/window closes |
| Shared across tabs | Yes (same origin) | No — isolated per tab |
| Survives reload | Yes | Yes |
| Survives browser restart | Yes | No |
| Sent to server | Never | Never |
Use localStorage for long-lived preferences (theme, language, “remember me”). Reach for sessionStorage for transient state that should not leak between tabs, like a multi-step wizard or a one-time-use token.
Storing objects with JSON
Storage only holds strings. Anything else is coerced via String(), so an object becomes the useless "[object Object]". To persist structured data, serialize it with JSON.stringify on the way in and JSON.parse on the way out.
const user = { id: 7, name: "Ada", roles: ["admin"] };
localStorage.setItem("user", JSON.stringify(user));
const raw = localStorage.getItem("user");
const restored = raw ? JSON.parse(raw) : null;
console.log(restored.name, restored.roles[0]);
Output:
Ada admin
A small wrapper makes this safe and reusable, guarding against missing keys and malformed JSON:
const store = {
get(key, fallback = null) {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback;
}
},
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
};
store.set("cart", [{ sku: "A1", qty: 2 }]);
console.log(store.get("cart")); // [ { sku: "A1", qty: 2 } ]
console.log(store.get("none", [])); // []
Limits and constraints
Web Storage is intentionally small and synchronous. Keep these boundaries in mind:
- ~5 MB per origin in most browsers (some allow up to 10 MB). Exceeding the quota throws a
QuotaExceededError. - Strings only — numbers, booleans, dates, and objects must be serialized.
- Synchronous — every read and write blocks the main thread, so avoid storing large blobs.
- Same-origin —
https://a.comandhttps://b.com(andhttpvshttps) have separate, isolated stores.
try {
localStorage.setItem("big", "x".repeat(10 * 1024 * 1024));
} catch (err) {
if (err.name === "QuotaExceededError") {
console.log("Storage full — evict something first");
}
}
Output:
Storage full — evict something first
A live counter demo
This self-contained pen reads a count from localStorage on load, increments it on each click, and persists it — refresh the result pane and the value sticks.
<button id="inc">Clicks: 0</button>
<button id="reset">Reset</button>
<script>
const btn = document.getElementById("inc");
const reset = document.getElementById("reset");
const render = () => {
const n = Number(localStorage.getItem("clicks") || 0);
btn.textContent = `Clicks: ${n}`;
};
btn.addEventListener("click", () => {
const n = Number(localStorage.getItem("clicks") || 0) + 1;
localStorage.setItem("clicks", String(n));
render();
});
reset.addEventListener("click", () => {
localStorage.removeItem("clicks");
render();
});
render();
</script>
Reacting to the storage event
When localStorage changes, the browser fires a storage event on every other tab of the same origin — but not on the tab that made the change. This is the built-in way to keep multiple tabs in sync, for example logging the user out everywhere when one tab signs out.
window.addEventListener("storage", (event) => {
console.log(event.key, event.oldValue, "->", event.newValue);
});
The StorageEvent carries key, oldValue, newValue, url, and the affected storageArea. Note that sessionStorage changes do not propagate this way, since each tab has its own session store.
The demo below logs whenever storage changes. Open the rendered page in two browser tabs, type in one, and watch the other update.
<input id="msg" placeholder="Type to broadcast across tabs" />
<pre id="log">Open this pen in two tabs…</pre>
<script>
const input = document.getElementById("msg");
const log = document.getElementById("log");
input.addEventListener("input", () => {
localStorage.setItem("shared-msg", input.value);
});
window.addEventListener("storage", (event) => {
if (event.key === "shared-msg") {
log.textContent = `Other tab says: ${event.newValue}`;
}
});
</script>
Best Practices
- Always namespace keys (e.g.
"app:theme") to avoid collisions with other scripts on the same origin. - Treat every
getItemresult as possiblynulland validate before parsing. - Wrap
JSON.parseintry/catch— stored data can be corrupted or hand-edited. - Wrap
setItemintry/catchto handleQuotaExceededErrorgracefully. - Never store secrets, tokens, or sensitive data — Web Storage is readable by any script on the page (XSS-exposed).
- Prefer
sessionStoragefor per-tab transient state andlocalStoragefor durable preferences. - Use the
storageevent to keep multiple tabs consistent instead of polling.