Topic 11.3
localStorage and sessionStorage
In one line
localStorage keeps small string key–value pairs for an origin across visits, sessionStorage keeps them for one tab; both store only strings (so use JSON), hold about 5 MB, block while they work, and must never hold secrets, because any script on the page can read them.
Think of it like this
A notebook left at the shop counter. Every time you visit the same shop (the same origin), the notebook is still there with what you wrote last time: that's localStorage. A sticky note on your table lasts only while you're sitting at that table, and goes in the bin when you leave: that's sessionStorage. Anyone working behind the counter can read the notebook, so you'd never write your bank PIN in it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Web Storage
- The browser's simple key–value storage for a page:
localStorageandsessionStorage. - Key and value
- A name (key) and the text stored under it (value), like a label on a box and what's inside.
- localStorage
- Storage for an origin that lasts across visits until it is cleared.
- sessionStorage
- Storage for one browser tab that is deleted when the tab closes.
- Serialise
- Turn a value into text so it can be stored or sent;
JSON.stringifydoes this. - Quota
- The maximum amount a site may store. Going over it makes
setItemthrowQuotaExceededError. - Synchronous
- The code waits right there until the work is done, blocking everything else on the page.
- IndexedDB
- A bigger, asynchronous database built into the browser, for lots of structured data.
- HttpOnly cookie
- A cookie the server sets that JavaScript cannot read, which keeps login sessions away from injected scripts.
Step by step
01Save and read a string
The simplest use: remember one setting. getItem gives back exactly the string you stored, or null if nothing was stored.
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // "dark"
const lang = localStorage.getItem("lang"); // null: never stored02Store objects as JSON
Turn the object into JSON text before saving and parse it when loading. Use ?? default for the missing case.
const settings = { theme: "dark", fontSize: 18 };
localStorage.setItem("settings", JSON.stringify(settings));
const saved = JSON.parse(localStorage.getItem("settings") ?? "{}");
console.log(saved.fontSize + 2); // 20, a real number again03Load safely with defaults
A loader that never throws and always returns a complete object is worth writing once and reusing.
const DEFAULTS = { theme: "light", fontSize: 16 };
function loadSettings() {
try {
const raw = localStorage.getItem("settings");
return { ...DEFAULTS, ...(raw ? JSON.parse(raw) : {}) };
} catch {
return { ...DEFAULTS }; // broken JSON or storage blocked
}
}04Pick the right place for the data
Each storage has its own lifetime and reach. Choose by asking "how long should this live, and who should see it?"
05Sync between tabs
The storage event fires in the other tabs of the same origin, not in the tab that made the change. It doesn't fire for sessionStorage changes in other tabs, because those tabs have their own copy. (The Hectal preview is a single sandboxed page, so you can't see this event here; try it on a real site with two tabs.)
window.addEventListener("storage", (e) => {
if (e.key === "theme") applyTheme(e.newValue);
if (e.key === "token" && e.newValue === null) showLoggedOut();
});06Look inside with DevTools
DevTools → Application (Chrome, Edge) or Storage (Firefox, Safari) → Local Storage → your origin. You can read, edit and delete entries there, which is exactly why your code must cope with values it didn't write.
Try it yourself
- 1
Fix the boolean
In the first example, predict what
localStorage.getItem("isAdmin") === "false"prints, then add it. Then storeisAdminwithJSON.stringify(false)and read it withJSON.parse: what does the ternary print now? - 2
Add a setting
In the theme example, add
compact: falsetoDEFAULTSand logloadSettings()again after saving. Predict: does the new key appear even though the stored JSON doesn't have it yet? Why? - 3
Watch the storage event for real
Open any site in two tabs. In tab A's console run
addEventListener("storage", (e) => console.log(e.key, e.newValue)). In tab B's console runlocalStorage.setItem("hello", "there"). Tab A printshello there; tab B prints nothing.
Code & diagrams
`false` was stored as the string `"false"`, which is truthy. The object became `"[object Object]"`. JSON fixes both. Real browsers may list keys in a different order; never rely on it.
Preview
Console
Nothing logged yet.Expected console output
string 51
truthy!
[object Object]
null
6
Asha
keys: 3
count
isAdmin
userThe broken saved value didn't crash the page: the loader fell back to defaults. Tick the box: the page turns dark and the stored JSON updates. On a real site it would still be dark after a reload.
Preview
Console
Nothing logged yet.Expected console output
bad saved settings, using defaults: SyntaxError
{ dark: false, size: 16 }
{"dark":false,"size":16}The code fakes one keystroke and one click so the console has something to show. Type in the box yourself afterwards and watch the status line.
Preview
Console
Nothing logged yet.Expected console output
draft: Hello Asha
after send: nullBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Parse a value that was never JSON
Store localStorage.setItem("theme", "dark") and later read it with JSON.parse(localStorage.getItem("theme")).
Break #2
Fill up the quota
On a real site, run localStorage.setItem("big", "x".repeat(6 * 1024 * 1024)) (Hectal's in-memory preview has no limit, so do this in a normal tab).
Myth vs fact
Myth
localStorage is a safe place for a login token because only my site can read it.
Fact
Only your origin can read it, but that includes every script running on your page: your code, third-party widgets, and any script an attacker injects through XSS. HttpOnly cookies are out of JavaScript's reach.
Myth
sessionStorage is shared between tabs of the same site.
Fact
Each tab has its own. Opening the same page in a new tab starts with empty sessionStorage (only duplicating a tab copies it).
Myth
localStorage keeps data forever.
Fact
Users clear it, private windows discard it, browsers may evict it when the disk is full, and Safari removes script-written storage for sites not visited for about seven days. Treat it as a cache.
Myth
localStorage.theme = "dark" is the same as setItem.
Fact
It often works, but it collides with method names (localStorage.key = 1 breaks key()), and some environments, including Hectal's preview, don't support it. Use setItem/getItem.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Storage is partitioned: in modern browsers a third-party iframe's storage is keyed by the top-level site too, so an embed on
a.comand the same embed onb.comsee different data. This blocks cross-site tracking through storage. - ▸
Because Web Storage is synchronous and on the main thread, Chrome loads an origin's whole
localStorageinto memory on first access. Reading it during start-up is cheap after that, but large values still cost memory and serialisation time. Batch writes (for example, debounce saves) instead of writing on every keystroke. - ▸
navigator.storage.estimate()reports usage and quota for all storage (IndexedDB, Cache API and so on), andnavigator.storage.persist()asks the browser not to evict your data under storage pressure. Web Storage isn't available in Web Workers or Service Workers; IndexedDB is. - ▸
Server-rendered apps (Next.js and similar) can't read
localStorageon the server. Reading it during the first render causes hydration mismatches; read it in an effect or a store after mount, and use a small inline script in the<head>for things like theme to avoid a flash.
Remember this
- 1
Web Storage is two objects the browser gives every page:
localStorageandsessionStorage. Both work the same way: you store a value under a key (a name), like a labelled box. The methods aresetItem(key, value),getItem(key)(returnsnullif the key doesn't exist),removeItem(key),clear()(removes everything), pluslengthandkey(index)to list what's there. Storage belongs to the origin (scheme + host + port, Topic 11.1):https://hectal.incan't see whathttps://example.comstored. - 2
**
localStoragelasts until the code or the user clears it: across reloads, closed tabs and browser restarts. It's shared by every tab of the same origin.sessionStoragebelongs to one tab**: it survives reloads in that tab, but another tab of the same site gets its own empty copy, and it's deleted when the tab closes. (Duplicating a tab copies it.) UselocalStoragefor preferences such as theme or language; usesessionStoragefor things that belong to one visit, such as a half-filled form or the step of a wizard. - 3
Web Storage stores only strings. Any value is converted with
String():5becomes"5",truebecomes"true", and an object becomes the useless text"[object Object]". So structured data goes through JSON:setItem("settings", JSON.stringify(settings))to save,JSON.parse(getItem("settings"))to load. Numbers need converting back too:Number(getItem("count")). Forgetting this gives bugs like"5" + 1 === "51". - 4
Treat stored data as untrusted and possibly missing. The user can edit it in DevTools, an older version of your app may have saved a different shape, and
JSON.parsethrows on broken text. A safe loader reads the key, returns a default when it'snull, wrapsJSON.parseintry/catch, and merges with defaults so new settings get sensible values. Writing can fail too: when the quota is full,setItemthrows aQuotaExceededError, and some privacy settings block storage entirely (accessing it throws aSecurityError). - 5
The limits: about 5 MB per origin in current browsers (counted in characters, so roughly 5 million characters), and the API is synchronous: every call blocks the main thread while the browser reads or writes, so storing large data on every keystroke can make typing laggy. For big or structured data (many records, files, offline apps) use IndexedDB, which is asynchronous and can hold far more. When another tab changes
localStorage, every other tab of that origin gets astorageevent withkey,oldValueandnewValue, handy for syncing a logout or theme across tabs. - 6
Never store secrets (passwords, long-lived access tokens, personal data) in Web Storage. Any JavaScript running on your page can read it, so a single XSS bug (cross-site scripting: an attacker getting their script to run on your page, Topic 8.3) leaks everything stored there. Login sessions are safer in cookies marked
HttpOnly(JavaScript can't read them),SecureandSameSite. Also remember that Safari may delete script-written storage for sites the user hasn't visited in about a week, and private windows throw it away at the end of the session: storage is a cache of convenience, not a database.
Explain it without notes
Compare localStorage, sessionStorage and cookies: lifetime, scope, size and who can read them.
Why do you need JSON.stringify and JSON.parse with Web Storage, and what goes wrong without them?
Why shouldn't you store an authentication token in localStorage?
How does the storage event work?
Practice
Write saveList(key, array) and loadList(key) that store an array as JSON. loadList must return [] when nothing is stored or the stored text is broken.
Make a visit counter: read visits from localStorage, add 1, save it, and print Visit number N. (In the preview it always prints 1, because storage resets on each Run.)
Trade-offs
- ↔
Web Storage is tiny and simple but synchronous and string-only; IndexedDB handles large structured data asynchronously but has a clunky API (small wrappers such as
idb-keyvalhelp). - ↔
Saving on every change keeps nothing lost but costs main-thread time; debouncing saves is faster but can lose the last second of edits if the tab is killed, so also save on
pagehide.
Done when you can
I can save, read, remove and list keys with
setItem,getItem,removeItem,lengthandkey().I store objects and numbers as JSON and parse them back.
I write loaders that survive missing or broken values.
I can choose between
localStorage,sessionStorage, cookies and IndexedDB.I never put secrets in Web Storage, and I can explain why.