JAVASCRIPT / THE BROWSER: DOM, EVENTS, AND STORAGE
Web Storage: localStorage against sessionStorage
Choose between localStorage and sessionStorage by lifetime and scope, store objects as JSON, and read missing keys without breaking your page.
What you will learn
- Pick localStorage for data that must outlive the tab, sessionStorage for per-tab state
- Round-trip objects with JSON.stringify and JSON.parse; Storage holds only strings
- Treat a null from getItem as 'nothing stored' and supply a fallback
- Sync tabs with the window storage event, which never fires in the writing tab
Understanding Web Storage: localStorage against sessionStorage
localStorage and sessionStorage are two instances of the same interface, Storage, hanging off window. They expose exactly the same methods, setItem, getItem, removeItem, clear and key, plus a length property, so nothing you learn about one is new for the other. What differs is which bucket the browser hands you: localStorage is keyed by origin alone and survives closing the browser, while sessionStorage is keyed by origin and by the tab, and the browser throws it away when that tab closes. Reloading or navigating inside the same tab is not closing it, so per-tab data survives a refresh.
Storage keeps strings and nothing else. setItem runs its value through string conversion, so 42 comes back as '42', true comes back as 'true', and a plain object comes back as [object Object] because that is what its default toString produces. Reading a key that was never set returns null rather than undefined, and that null is your only reliable signal for 'nothing here'. The convention is JSON.stringify on write and JSON.parse on read inside a try/catch, because JSON.parse(null) quietly returns null while JSON.parse of any non-JSON text throws SyntaxError.
Both APIs are synchronous: a setItem finishes writing before the next line runs, which is why saving large values inside an input or scroll handler makes typing or scrolling stutter. Because localStorage is shared live by every tab on the origin, the browser dispatches a storage event on the other tabs' window objects after each change, and the tab that wrote never hears its own event. sessionStorage's isolation is the entire point of it: two tabs can each hold a different half-finished checkout without overwriting each other. Neither store is a place for secrets, since any script running on the page, including one injected through XSS, can read every key.
// Run this on a scratch page served over http/https, not from about:blank.
// clear() deletes everything this origin has stored, so use a page you own.
localStorage.clear();
sessionStorage.clear();
localStorage.setItem('theme', 'dark');
sessionStorage.setItem('theme', 'light');
// Same methods, two completely separate buckets.
console.log(localStorage.getItem('theme'));
console.log(sessionStorage.getItem('theme'));
// Values are converted to strings on the way in.
sessionStorage.setItem('count', 7);
const raw = sessionStorage.getItem('count');
console.log(typeof raw, raw + 1);
// A key that was never set reads back as null, not undefined.
console.log(localStorage.getItem('nope'));
console.log(localStorage.length, sessionStorage.length);localStorage and sessionStorage are the same string-only key/value API, and only the bucket's lifetime and scope differ: per origin and permanent versus per tab and discarded with the tab.
Worked examples
Objects need JSON, reads need a fallback
Shows what happens when an object is stored directly, and a read helper that survives both a missing key and a corrupt value.
sessionStorage.clear();
const prefs = { theme: 'dark', fontSize: 16, tags: ['new', 'sale'] };
sessionStorage.setItem('bad', prefs);
console.log(sessionStorage.getItem('bad'));
sessionStorage.setItem('good', JSON.stringify(prefs));
const back = JSON.parse(sessionStorage.getItem('good'));
console.log(back.fontSize + 1, Array.isArray(back.tags));
function read(key, fallback) {
const raw = sessionStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
console.log(read('missing', 'default'));
console.log(read('bad', 'default'));Example explained
Line 1setItem('bad', prefs) calls the object's default toString, so the stored text is literally [object Object] and the keys are lost.
Line 2JSON.stringify preserves types, so back.fontSize is a number again (17) and back.tags is a real array.
Line 3read('missing') sees getItem return null and returns the fallback without ever calling JSON.parse.
Line 4read('bad') gets [object Object], which is not valid JSON, so the catch turns the SyntaxError into the same fallback.
What survives a reload and what does not
Two counters that separate origin-wide persistence from per-tab persistence.
// First run in a brand-new tab, with nothing stored for this origin yet.
const visits = Number(localStorage.getItem('visits') || 0) + 1;
const views = Number(sessionStorage.getItem('views') || 0) + 1;
localStorage.setItem('visits', visits);
sessionStorage.setItem('views', views);
console.log('visits on this origin:', visits);
console.log('views in this tab:', views);Example explained
Line 1getItem returns null on the first run, and null || 0 gives 0, so both counters start at 1.
Line 2setItem converts the number to a string, and Number() converts it back on the next run.
Line 3Reload the tab and both print 2, because a reload destroys neither storage area.
Line 4Close the tab and open the page again: visits keeps climbing while views restarts at 1, because that sessionStorage bucket died with the tab.
Cross-tab updates with the storage event
Demonstrates that a localStorage write notifies other tabs but not the tab that made the change.
// Load this page in two tabs of the same origin, then run the setItem line in the other tab.
window.addEventListener('storage', (event) => {
console.log('changed:', event.key, event.oldValue, '->', event.newValue);
});
localStorage.setItem('ping', '1');
console.log('setItem finished; the handler above did not run');Example explained
Line 1The listener goes on window, not on the localStorage object, which has no addEventListener of its own.
Line 2The write succeeds, but the storage event is dispatched only to other same-origin documents, so this tab logs nothing from the handler.
Line 3Run localStorage.setItem('ping', '2') in the second tab and this tab prints: changed: ping 1 -> 2.
Line 4A sessionStorage write can never reach another tab, so this synchronisation pattern only works with localStorage.
Important notes
Both buckets are keyed by full origin, so http://site.com, https://site.com and https://app.site.com each get their own storage, and pages on an opaque origin such as about:blank have none to use.
Space is limited to roughly 5 MB per origin and setItem throws QuotaExceededError past that; merely touching localStorage can throw SecurityError when site data is blocked, for example inside a third-party iframe.
Common mistakes
Passing an object straight to setItem: the value becomes the string [object Object], and the JSON.parse on the next read throws SyntaxError, so the data is unrecoverable.
Using getItem's return value as a boolean or number: the strings 'false' and '0' are both truthy, so if (localStorage.getItem('muted')) runs even when the stored value says the opposite.
Assuming sessionStorage is shared across tabs: a link opened in a new tab starts with an empty bucket, so wizard or login state saved there reads back as null while the original tab still has it.
Try it yourself
Change, predict, then run
On a scratch page, add a textarea that saves its value to sessionStorage on every input event and restores it when the page loads. Then change that one key to localStorage and note which of a reload, a new tab, and a tab close still shows your text.
Open the JavaScript workspaceCheck your understanding
A multi-step form saves progress under the key 'draft'. The user is on step 3, copies the page URL into a brand-new tab of the same site, and also reloads the original tab. Which statement is true?
- With 'draft' in sessionStorage, the reloaded original tab still reads it while the new tab reads null.
- With 'draft' in sessionStorage, both tabs read it, because the two tabs share the same origin.
- With 'draft' in localStorage, only the new tab reads it, because localStorage is created per tab.
- Either store loses 'draft' on reload, because reloading a page discards its storage areas.
Show answer
sessionStorage is scoped to a tab's session and survives reloads and same-tab navigation, so the original tab keeps the draft, while a freshly opened tab starts with an empty bucket and getItem returns null. Option 2 is tempting because both stores really are origin-scoped, but sessionStorage adds a second scope on top of the origin, the tab itself; and no reload clears either store, which rules out option 4.