localStorage saves small pieces of data in the browser so they survive a refresh, a closed tab, and a restarted computer.
localStorage.setItem("name", "Ada");
localStorage.getItem("name"); // "Ada"
localStorage.removeItem("name");Three methods and one important limitation: it only stores strings. Everything else on this page follows from that.
Everything becomes a string#
This catches everyone once:
localStorage.setItem("count", 5);
const count = localStorage.getItem("count");
console.log(count); // "5" — a string
console.log(count + 1); // "51" — glued, not added
console.log(Number(count) + 1); // 6Booleans are worse, because the mistake is silent:
localStorage.setItem("done", false);
if (localStorage.getItem("done")) {
// this ALWAYS runs — "false" is a non-empty string, which is truthy
}Storing objects and arrays#
Convert to JSON on the way in and back on the way out.
const tasks = [
{ id: 1, text: "Learn storage", done: false },
];
localStorage.setItem("tasks", JSON.stringify(tasks));
const loaded = JSON.parse(localStorage.getItem("tasks"));
console.log(loaded[0].text); // "Learn storage"Forget JSON.stringify and the browser calls toString() on your array, which quietly stores "[object Object]". It does not error. You find out later when the load produces nonsense.
The trap: loading breaks the whole app#
This is the one worth learning properly, because it fails at startup and takes the page down with it.
const tasks = JSON.parse(localStorage.getItem("tasks")); // fragileTwo ways that line breaks:
- Nothing is stored yet.
getItemreturnsnull, andJSON.parse(null)givesnull— so the next line calling.map()throws “Cannot read properties of null”. - The stored value is not valid JSON. A half-written value, something you edited by hand while testing, or a leftover from an earlier version of your code.
JSON.parsethrows aSyntaxErrorand your script stops before rendering anything.
Once that happens, the page is broken on every load until the user clears their site data — which they will not know to do. Guard it:
function load(key, fallback) {
try {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
const value = JSON.parse(raw);
return value ?? fallback;
} catch {
localStorage.removeItem(key); // it is unusable — clear it
return fallback;
}
}
const tasks = load("tasks", []);A save/load pair worth copying#
const KEY = "todo-tasks-v1";
function save(tasks) {
try {
localStorage.setItem(KEY, JSON.stringify(tasks));
} catch {
// Storage full, or blocked in this browser. Not fatal — carry on.
}
}
function load() {
try {
const raw = localStorage.getItem(KEY);
return raw ? JSON.parse(raw) : [];
} catch {
localStorage.removeItem(KEY);
return [];
}
}Note that setItem is in a try too. It throws if storage is full, and in some browsers it throws simply because the user has blocked site data. An app that crashes on save in private browsing is a real and avoidable bug.
This is exactly the pattern in the browser to-do list project.
What localStorage is not#
- Not secure. Anyone can open DevTools and read or edit it. Never put passwords, tokens or anything you would not show the user in there.
- Not synced. It lives in one browser on one device. Different browser, different data. Clearing site data deletes it.
- Not shared between sites. Each origin gets its own. Your local test copy and the deployed site have separate storage.
- Not a database. Roughly 5MB, no querying, and every read and write is synchronous — it blocks the page. Fine for settings and a few hundred items; wrong for thousands of records.
- Not reliable. Treat everything you read back as possibly missing or possibly wrong.
The rest of the API#
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark", or null if absent
localStorage.removeItem("theme");
localStorage.clear(); // everything for this site
localStorage.length; // how many keys
localStorage.key(0); // the name of the first key
// Loop over everything
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
console.log(k, localStorage.getItem(k));
}Be careful with clear() — it wipes everything your site stored, not just your key.
sessionStorage#
Identical API, different lifetime: sessionStorage is cleared when the tab closes, and it is not shared between tabs.
sessionStorage.setItem("step", "3");Use it for things that should not outlive the visit — a multi-step form’s progress, a temporary filter.
Reacting to changes in another tab#
window.addEventListener("storage", (event) => {
if (event.key === "todo-tasks-v1") {
render(JSON.parse(event.newValue || "[]"));
}
});The storage event fires in other tabs on the same site, not the one that made the change. It is a simple way to keep two open tabs in step.
Questions people ask#
How much can I store?
Around 5MB per site in most browsers. Exceeding it throws a QuotaExceededError on setItem, which is why the save function catches.
Does localStorage work offline?
Yes — it is local to the browser and needs no network at all.
Should I use cookies instead?
Cookies are sent to the server on every request, which makes them right for sessions and authentication and wrong for app state. localStorage never leaves the browser.
What about IndexedDB?
A proper browser database — asynchronous, much larger, supports indexes. Worth reaching for when you outgrow localStorage, which for most small projects is never.
Why is my data gone?
Usual causes: the user cleared site data, they are in private browsing, you are on a different origin (localhost versus a deployed URL, or http versus https), or you changed the key name.
Can I store an image?
Only as a base64 data URL, and it will eat your quota quickly. For anything more than an icon, store a URL and let the browser cache the file.
Where to go next#
- Build a to-do list that saves its tasks
- JavaScript array methods reference
- Debugging JavaScript in the browser — the Application panel shows your storage