localStorage & sessionStorage
Learn the browser Storage API: saving strings with localStorage and sessionStorage, storing JSON safely, reacting to storage events, and knowing when not to use it.
- 01Save and read small valuesUse
setItem,getItem,removeItem,key(index), andlengthwithout wiping unrelated site data. - 02Store real data safelyRound-trip objects with JSON, handle corrupt values, and migrate versioned settings.
- 03Choose the right browser storageExplain storage events, quotas, sync costs, security risks, and alternatives.
Two tiny browser shelves
Web Storage is a pair of simple browser APIs for saving small pieces of text on one origin: the exact combination of scheme, host, and port, such as https://example.com. The two shelves have the same methods but different lifetimes.
localStoragekeeps values after reloads, browser restarts, and return visits until your code or the user removes them.sessionStoragekeeps values for one top-level tab. Reloading keeps them; closing the tab removes them. In most browsers, duplicating a tab starts with a copy.
Imagine each website origin gets its own tiny office. The office has a small labeled drawer for values that should still be there next week. Each open tab also has a sticky note on its own monitor. A second tab has its own sticky note, even though it works in the same office.
- In real life: A labeled drawer in this website’s office
- In JavaScript:
localStorage: this origin’s long-lived key/value shelf - In real life: Only this office’s staff can open it
- In JavaScript: Only pages from the same origin can read those keys
- In real life: A sticky note on this tab’s monitor
- In JavaScript:
sessionStorage: per-tab data that survives reloads - In real life: Throw away the monitor note when the tab closes
- In JavaScript: Closing the tab ends that session storage
Where the analogy stops: Real drawers can hold anything. Web Storage can hold only strings, is synchronous, and is not a safe place for secrets.
Use Web Storage for preferences, UI state, feature tour flags, and small drafts that only the browser needs. Do not treat it like a database, a secure vault, or a server communication channel.
setItem, getItem, keys and removal
INTERACTIVEBoth storage objects implement the same tiny interface. You write with setItem(key, value), read with getItem(key), remove one key with removeItem(key), inspect the number of stored keys with length, and ask for the key at an index with key(index). There is also clear(), but production code should use it carefully because it wipes every key for this origin, not only yours.
localStorage.theme = "dark" may appear to work, but setItem("theme", "dark") is explicit, consistent with getItem, and avoids surprises with built-in property names.
const storage = localStorage; storage.setItem("count", 7);storage.setItem("darkMode", false);storage.setItem("profile", { name: "Ada" }); console.log(storage.getItem("count"));console.log(storage.getItem("darkMode"));console.log(storage.getItem("profile"));console.log(storage.getItem("missing"));No cf-demo keys are currently stored.
Click a button to write demo keys for this origin only.
The important rule is that keys and values are strings. The browser applies String(value) before saving. A number comes back as "7", a boolean as "false", and a plain object as "[object Object]". Missing keys return null, which is why you will often see localStorage.getItem("tab") ?? "overview".
Think of Web Storage as a locker that only fits written notes. Put object data into a JSON note first, then read and parse that note when you need it again.
- In real life: A note fits in the locker
- In JavaScript: A string such as
dark - In real life: Write object data as a JSON note
- In JavaScript:
JSON.stringify(settings) - In real life: Read the JSON note back
- In JavaScript:
JSON.parse(saved)
Where the analogy stops: JSON handles plain data, not every JavaScript value. Functions, symbols, Maps, Sets, and Dates need special handling.
Storing JSON safely
INTERACTIVEThe fix for plain objects is the JSON lesson’s pattern: turn data into text with JSON.stringify, then turn the text back into data with JSON.parse. Because stored text can be missing, old, manually edited in DevTools, or left behind by an older version of your app, read it defensively with try/catch.
function readJSON(key, fallback) { const text = localStorage.getItem(key); if (text === null) return fallback; try { return JSON.parse(text); } catch { return fallback; }} const oldSettings = { version: 1, data: { theme: "dark", compact: true }};localStorage.setItem("cf-demo:settings", JSON.stringify(oldSettings)); const loaded = readJSON("cf-demo:settings", { version: 2, data: {} });const current = loaded.version === 1 ? { version: 2, data: { ...loaded.data, textSize: "normal" } } : loaded; localStorage.setItem("cf-demo:settings", JSON.stringify(current));{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}string{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}Version 1 data loaded safely, migrated to version 2, then saved back as JSON text.
Versioning matters as soon as real users have saved data. Store an envelope like { version: 2, data: settings }. When you load an older version, migrate it to the newest shape before the rest of your app uses it. That keeps the messy compatibility code in one place.
| Value | Stored JSON text | What to remember |
|---|---|---|
| Plain object | {"theme":"dark"} | Use JSON.stringify before writing and JSON.parse after reading. |
| Missing key | null from getItem | Return a fallback instead of parsing null accidentally. |
| Corrupt text | {broken | Catch JSON.parse errors and recover. |
| Date object | A string such as 2026-09-25T00:00:00.000Z | JSON does not restore a real Date; recreate one if you need date methods. |
The storage event
INTERACTIVEWhen one document changes localStorage or sessionStorage, other same-origin documents can receive a storage event. The document that made the change does not receive its own event. That keeps it useful for cross-tab coordination, not as a replacement for your current component’s state update.
Think of changing the drawer and shouting “I changed the drawer!” down the hallway. Other rooms can react. Your own room already knows because it made the change.
- In real life: You change the labeled drawer
- In JavaScript: One tab calls
localStorage.setItem - In real life: A colleague in another room hears the shout
- In JavaScript: Another same-origin tab or iframe receives
storage - In real life: You do not hear your own shout
- In JavaScript: The writing document gets no event
- In real life: The shout names what changed
- In JavaScript: The event includes
key,oldValue,newValue,url, andstorageArea
Where the analogy stops: The event is not a reliable message bus for large data or high-frequency updates. Use dedicated channels when coordination becomes complex.
window.addEventListener("storage", (event) => { console.log(event.key); console.log(event.oldValue); console.log(event.newValue); console.log(event.url); console.log(event.storageArea === localStorage);}); localStorage.setItem("cf-demo:signal", "hello");The iframe reports changes because it is another same-origin document. This page's own storage event count is 0.
If clear() is used, event.key is null. In this lesson’s demos we avoid localStorage.clear() and remove only keys that start with cf-demo:, because this website may have its own unrelated keys such as theme preferences.
Pattern: remember the last tab
STEP THROUGHA common use is remembering the last tab, panel, or filter a visitor chose. The pattern is deliberately small: read with a fallback, let the user choose, write the new string, then read it on the next visit. The execution below is recorded from a testable in-memory Storage model, because Node does not provide browser localStorage by default.
Step through a practical pattern: read a saved value with a fallback, let the user choose, write the new value, then restore it later.
script
const fallback = "overview";let activeTab = storage.getItem("lastTab") ?? fallback; activeTab = "examples";storage.setItem("lastTab", activeTab); const nextVisit = storage.getItem("lastTab") ?? fallback;console.log(nextVisit);Notice that storage is not the live source of truth for every click. Your UI state changes immediately, then you persist the latest choice. On page load, storage simply helps choose the initial state.
Limits and when not to use it
HONEST EDGESWeb Storage is convenient, but it is intentionally small and simple. Browsers commonly allow around a few megabytes per origin, often around 5 MB, but the real limit varies. setItem can throw a QuotaExceededError DOMException when the browser refuses a write, so production code should catch it and degrade gracefully.
try { localStorage.setItem("cf-demo:settings", jsonText);} catch (error) { // QuotaExceededError or storage unavailable: fall back gracefully.}Press Test to try one temporary demo write, then remove it immediately.
Press Test to try one temporary demo write, then remove it immediately.
| Concern | Why it matters | Better choice |
|---|---|---|
| Secrets and tokens | Any script running on the page can read Web Storage, so an XSS bug can steal it. | Avoid storing secrets there; server-managed HttpOnly cookies are often safer for session secrets. |
| Large or structured data | Everything is a string and the synchronous API can block the main thread. | IndexedDB for records, indexes, blobs, and larger offline data. |
| Server needs the value | Web Storage is not sent with HTTP requests. | Cookies for small server-needed values. |
| Workers | localStorage and sessionStorage are Window APIs, not Worker APIs. | IndexedDB or messages between the worker and window. |
| Private browsing | Some browsers clear data sooner or restrict persistence. | Feature-detect, catch errors, and treat storage as optional. |
Never store passwords, raw access tokens, payment details, or other secrets in localStorage or sessionStorage. If malicious script runs on your page, it can read them.
localStorage, sessionStorage, IndexedDB, or cookie?
SORT ITThe next lesson is IndexedDB, a real browser database. For now, keep the decision tree simple: Web Storage is for small client-only strings, cookies are for tiny values the server needs, and IndexedDB is for larger structured client data.
- Remember this site’s light/dark theme after the browser restarts
- Keep a checkout wizard’s current step only for this tab
- Store hundreds of offline notes with indexes and structured records
- Send a tiny language preference with the next server request
- Save an access token that would be dangerous if script could read it
- Remember a product filter panel’s choices on this origin
- Keep a preview mode separate in two same-site tabs
- Cache large blobs or files for offline use
Sort each realistic scenario into the storage option you would reach for first.
Common misconceptions
- “It stores JavaScript values.” It stores strings. Use JSON for plain data and rebuild special types like
Datedeliberately. - “sessionStorage disappears on refresh.” It survives reloads. It disappears when that top-level tab session ends.
- “Every tab shares sessionStorage.” Tabs on the same origin share
localStorage, but each top-level tab gets its ownsessionStoragearea. - “The storage event tells my current page it wrote.” It fires in other same-origin documents, not the writer.
- “It is safe because users cannot see it.” Users and scripts can inspect it. Never put secrets there.
- “clear() is a harmless reset.” It wipes the whole storage area for the origin. Prefer removing your own namespaced keys.
Practice exercises
5 EXERCISESPredict the two console lines.
const storage = new Map();
storage.set("count", String(3));
storage.set("flag", String(false));
console.log(storage.get("count") + 2);
console.log(typeof storage.get("flag"));3 is stored as the string "3", so "3" + 2 prints 32. false is stored as the string "false", so typeof prints string.
Read the helper and predict the two themes it prints.
function readJSON(text, fallback) {
if (text === null) return fallback;
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(readJSON('{"theme":"dark"}', {}).theme);
console.log(readJSON('{broken', { theme: "light" }).theme);function readJSON(text, fallback) {
if (text === null) return fallback;
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(readJSON('{"theme":"dark"}', {}).theme);
console.log(readJSON('{broken', { theme: "light" }).theme);The valid JSON gives dark. The corrupt text throws during parsing, the catch block returns the fallback, and that fallback theme is light.
A developer writes a setting and waits for the same page’s storage listener to update the UI. Will that listener run in the writing page?
No. The document that calls setItem does not receive its own storage event. Update the current page directly, and use storage to hear about other tabs or frames.
Predict the output of the small Map-backed Storage model.
const storage = new Map();
const fallback = "home";
let tab = storage.get("tab") ?? fallback;
tab = "settings";
storage.set("tab", tab);
console.log(storage.get("tab") ?? fallback);The first read starts at home, then the user picks settings. The code stores that value, so the final read prints settings.
For a note-taking app, decide where to store: theme preference, current unsaved tab preview, hundreds of offline notes, and the signed-in session secret. Explain each choice.
A good plan might use localStorage for a theme, sessionStorage for one-tab wizard progress, IndexedDB for offline drafts, and an HttpOnly cookie managed by the server for a session secret. The important part is matching lifetime, size, server needs, and security.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does
getItemreturn when a key is missing?Read the code, then predictconst storage = new Map(); const getItem = (key) => storage.has(key) ? storage.get(key) : null; console.log(getItem("missing"));Choose an answer to see the explanation.
Question 2 of 7What does this print after storing a number?
Read the code, then predictconst value = String(42); console.log(typeof value); console.log(value + 1);Choose an answer to see the explanation.
Question 3 of 7Which value should you store for an object you want back later?
Choose an answer to see the explanation.
Question 4 of 7Where does a
storageevent fire after one document changeslocalStorage?Choose an answer to see the explanation.
Question 5 of 7Which statement about
sessionStorageis most accurate?Choose an answer to see the explanation.
Question 6 of 7What should production code do around
setItemwhen storage may be full?Choose an answer to see the explanation.
Question 7 of 7Which storage is best for a large offline collection of structured records?
Choose an answer to see the explanation.
Key takeaways
localStorageis per-origin and long-lived;sessionStorageis per-origin and per-tab.- Keys and values are strings. Missing
getItemresults arenull. - Use JSON, fallbacks,
try/catch, and version migrations for real data. - The
storageevent fires in other same-origin documents, not the writer. - Catch quota failures, avoid secrets, and choose IndexedDB or cookies when they fit better.
Final definition: Web Storage is a synchronous, string-only, per-origin browser key/value API for small client-side data that should live either across visits or within one tab session.
Up next: IndexedDB.