localStorage is a per-origin key and value store in the browser that keeps data after the tab and browser close. You call setItem(key, value) to save, getItem(key) to read, removeItem(key) to delete and clear() to empty it. Values are always strings, so wrap objects in JSON.stringify and JSON.parse.
| Member | What it does | Returns |
|---|---|---|
| setItem(key, value) | Adds or overwrites one entry | undefined |
| getItem(key) | Reads one entry | The string, or null if missing |
| removeItem(key) | Deletes one entry | undefined |
| clear() | Deletes every entry for this origin | undefined |
| key(index) | Name of the entry at a position | A string or null |
| length | How many entries are stored | A number |
Updated
Most localStorage bugs come from three surprises: everything turns into a string, the call can throw, and the data is tied to one exact origin. The playground below writes to your own browser so you can watch those rules happen, then the examples show safe patterns for preferences, a shopping cart and a theme switch.
Save, read and delete entries in your own browser and see every key this site stores.
| localStorage | sessionStorage | Cookie | |
|---|---|---|---|
| Lifetime | Until removed | Until the tab closes | Until its expiry date |
| Shared between tabs | Yes, same origin | No, one tab only | Yes |
| Sent to the server | No | No | Yes, on every request |
| Size (MDN figure) | 5 MiB per origin | 5 MiB per origin | Small, limits set per browser |
| API style | Synchronous | Synchronous | document.cookie string |
One place handles blocked storage, a full quota and broken JSON, so the rest of your code never has to.
export function load(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw === null ? fallback : JSON.parse(raw);
} catch {
return fallback; // blocked storage or invalid JSON
}
}
export function save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (err) {
console.warn("Could not save", key, err); // QuotaExceededError or SecurityError
return false;
}
}Defaults are merged with whatever was saved, so adding a new setting later does not break older saved data.
const DEFAULTS = { units: "metric", compact: false, version: 1 };
function getPrefs() {
return { ...DEFAULTS, ...load("app:prefs", {}) };
}
function setPref(name, value) {
const next = { ...getPrefs(), [name]: value };
save("app:prefs", next);
return next;
}
setPref("units", "imperial");
console.log(getPrefs().units); // "imperial"Each change creates a new array and writes it back. The server still decides prices at checkout.
const CART = "shop:cart";
function addToCart(id, qty = 1) {
const cart = load(CART, []);
const found = cart.find((line) => line.id === id);
const next = found
? cart.map((line) => (line.id === id ? { ...line, qty: line.qty + qty } : line))
: [...cart, { id, qty }];
save(CART, next);
return next;
}
function cartCount() {
return load(CART, []).reduce((sum, line) => sum + line.qty, 0);
}If nothing is saved, the operating system setting wins. Run this early in the head to avoid a flash of the wrong theme.
function applyTheme() {
let saved = null;
try { saved = localStorage.getItem("theme"); } catch {}
const dark = saved === "dark" ||
(saved === null && matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
}
applyTheme();localStorage has no built-in expiry, so store the deadline with the value and drop stale entries on read.
function saveFor(key, value, minutes) {
save(key, { value, expires: Date.now() + minutes * 60_000 });
}
function loadFresh(key) {
const item = load(key, null);
if (!item || Date.now() > item.expires) {
try { localStorage.removeItem(key); } catch {}
return null;
}
return item.value;
}The storage event reaches every other tab on the same origin, so a cart badge can update without a reload.
window.addEventListener("storage", (event) => {
if (event.key === "shop:cart") {
const cart = event.newValue ? JSON.parse(event.newValue) : [];
document.querySelector("#cart-count").textContent = String(cart.length);
}
});
It shines for small bits of interface memory: the last tab someone opened, a dismissed banner, a draft message, a preferred unit. Losing that data is mildly annoying rather than harmful, which matches how easily people clear it.
It is a poor fit for anything you must not lose, anything another device should see, or large collections. Orders belong on a server, and big structured data belongs in IndexedDB, which is asynchronous.
Browsers throw a SecurityError when storage is blocked, for example when a visitor has disabled site data, and pages opened from file: URLs behave differently between browsers. A full store throws a QuotaExceededError on write.
A small wrapper that returns a fallback value on failure keeps the rest of your page working. The examples below all use that shape.
Anything in storage can be read by any script on the origin, including a third-party widget or injected code from a cross-site scripting bug. Treat it as public to the page.
Data read back is also untrusted input. Validate it before use and render it as text, never as HTML.
MDN documents a limit of 5 MiB of localStorage per origin in current browsers, with a separate 5 MiB for sessionStorage. Strings are stored as UTF-16, so the number of characters you can fit is lower than the byte count suggests. Writing past the limit throws a QuotaExceededError.
No. Entries stay until your code removes them or the visitor clears site data. In a private window the browser wipes them when the last private tab closes. If you need an expiry, store a timestamp with the value and check it when you read.
setItem turns whatever you pass into a string. A plain object becomes the text [object Object] and the data is lost. Save JSON.stringify(obj) and read it back with JSON.parse.
No. Storage belongs to an origin, which is the scheme, host and port together. http://example.com and https://example.com each get their own separate store, and so do subdomains.
Listen for the storage event on window. It fires in the other open documents of the same origin when one of them changes a value, and carries the key, oldValue and newValue. The tab that made the change does not receive it.
Cookies travel to the server with every request and suit sessions. localStorage never leaves the browser, holds more, and suits interface state such as a theme, a draft or a collapsed sidebar.
Related: JavaScript fetch and async, JSON formatter, Add dark mode to a website, JavaScript event listeners, JavaScript string methods.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.