πΎ LocalStorage and SessionStorage
The browser can remember things for you β a chosen theme, an in-progress form, a shopping cart β without ever touching a server. The Web Storage API gives you a dead-simple key/value store that lives right on the user's device. In this lesson you'll learn when to reach for localStorage versus sessionStorage, how to store real objects safely, and the security lines you must never cross.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain how Web Storage differs from cookies and choose the right tool for a job
- Use
setItem,getItem,removeItem, andclearto manage stored data - Persist and rehydrate objects and arrays with
JSON.stringify/JSON.parse, including dates - Contrast localStorage (persistent) with sessionStorage (per-tab, temporary)
- Synchronise state across tabs with the storage event and handle
QuotaExceededErrorgracefully - Identify what should never be stored client-side and why
Estimated Time: 35β45 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a persistent "remember my preferences" panel backed by localStorage.
In This Lesson
What Is Web Storage?
The Web Storage API lets a website save data directly in the visitor's browser as simple key/value pairs. Both keys and values are always strings. It comes in two flavours that share one identical API but differ in how long the data lives:
localStorageβ persists indefinitely. It survives page reloads, tab closes, and even a full browser restart. It only goes away when your code deletes it or the user clears their browser data.sessionStorageβ lasts only for the lifetime of a single tab. Close the tab and it's gone.
π‘ A useful analogy:localStorageis the notebook you keep on your desk β write in it today, and it's still there next week.sessionStorageis a sticky note you scribble during one work session and toss when you get up. Cookies, by contrast, are tiny slips you carry back and forth to the server in your pocket every single trip.
Web Storage sits alongside several other client-side storage mechanisms. Knowing where each fits keeps you from reaching for the wrong one:
π Key Terms
Origin: the combination of scheme + host + port (e.g. https://shop.example.com:443). Storage is scoped to one origin and never shared with another.
Serialization: turning an in-memory value (like an object) into a string that can be stored, and back again.
Quota: the storage budget a browser grants an origin β commonly around 5 MB for Web Storage.
localStorage Fundamentals
The whole API is four methods plus a length property. Both keys and values must be strings β anything else is coerced to a string automatically, which is a common source of bugs.
// Store values (keys and values are strings)
localStorage.setItem('username', 'ray');
localStorage.setItem('darkMode', 'true'); // note: the string "true", not a boolean
// Read a value β returns null if the key does not exist
const name = localStorage.getItem('username'); // 'ray'
const missing = localStorage.getItem('nope'); // null
// A common gotcha: everything comes back as a string
const dark = localStorage.getItem('darkMode');
console.log(dark === true); // false! it's the STRING 'true'
console.log(dark === 'true'); // true
// Remove one key, or wipe the whole origin's storage
localStorage.removeItem('darkMode');
localStorage.clear();
// Inspect what is stored
console.log(localStorage.length); // number of keys
console.log(localStorage.key(0)); // the name of the first key
β οΈ Booleans and numbers become strings
localStorage.setItem('count', 5) stores the string "5". When you read it back you must convert it: Number(localStorage.getItem('count')). For booleans, compare against 'true' or store JSON. Forgetting this is the number-one Web Storage bug.
Handling a full quota
If you exceed the ~5 MB budget (or storage is disabled, as in some private-browsing modes), setItem throws a QuotaExceededError. Always wrap writes that could be large:
function safeSet(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.error('Storage is full β clearing old caches.');
// e.g. evict cached items, then retry, or fall back to memory
}
return false;
}
}
Storing Objects, Arrays & Dates
Because values must be strings, you can't store an object directly β setItem('user', {name:'Ray'}) would save the useless string "[object Object]". The fix is JSON serialization: stringify on the way in, parse on the way out.
const prefs = {
theme: 'dark',
fontSize: 16,
notifications: { email: true, push: false }
};
// Save: object -> JSON string
localStorage.setItem('prefs', JSON.stringify(prefs));
// Load: JSON string -> object (guard against missing/corrupt data)
const raw = localStorage.getItem('prefs');
const restored = raw ? JSON.parse(raw) : null;
console.log(restored?.notifications.email); // true
β οΈ Dates do not survive a round-trip
JSON.stringify turns a Date into an ISO string, and JSON.parse does not turn it back. You must rehydrate dates yourself.
const activity = { lastLogin: new Date() };
localStorage.setItem('activity', JSON.stringify(activity));
const back = JSON.parse(localStorage.getItem('activity'));
console.log(typeof back.lastLogin); // 'string' β not a Date!
// Rehydrate it
back.lastLogin = new Date(back.lastLogin);
console.log(back.lastLogin.getFullYear()); // now it's a real Date
For a cleaner, reusable approach, wrap the pattern in a small helper that adds JSON handling, optional expiry, and error safety. Modern codebases build exactly this kind of utility once and reuse it everywhere:
const store = {
// Save any JSON-serialisable value, with an optional TTL in minutes
set(key, value, ttlMinutes) {
const record = { value };
if (ttlMinutes) record.expiresAt = Date.now() + ttlMinutes * 60_000;
try {
localStorage.setItem(key, JSON.stringify(record));
} catch (err) {
console.error('store.set failed:', err);
}
},
// Read a value, honouring expiry; returns null if missing or stale
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const record = JSON.parse(raw);
if (record.expiresAt && Date.now() > record.expiresAt) {
localStorage.removeItem(key);
return null;
}
return record.value;
} catch {
return null; // corrupt JSON β treat as missing
}
},
remove(key) { localStorage.removeItem(key); }
};
store.set('cart', [{ id: 1, qty: 2 }]); // survives restarts
store.set('token', 'abc', 60); // auto-expires in 60 minutes
console.log(store.get('cart')); // [{ id: 1, qty: 2 }]
sessionStorage & Tab Isolation
sessionStorage has the exact same API as localStorage β swap the object name and everything works. The only difference is lifetime and scope: its data belongs to one browsing-context (tab) and vanishes when that tab closes.
// Perfect for one-visit, throwaway state
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('scrollY', String(window.scrollY));
const step = sessionStorage.getItem('checkoutStep'); // '2'
sessionStorage.clear(); // wiped automatically when the tab closes anyway
This tab-level isolation is a feature, not a limitation. Each tab gets its own private sessionStorage, so two tabs on the same site can run independent multi-step flows without stepping on each other. Meanwhile localStorage is shared across every tab of the origin:
sessionStorage, but every tab of the same origin reads and writes the one shared localStorage.π‘ Quirk: duplicating a tab
If a user duplicates a tab (right-click β Duplicate), the new tab starts with a one-time copy of the original's sessionStorage. From that moment the two are independent. Opening a fresh tab to the same URL, however, always starts empty.
Syncing Tabs with the storage Event
Here's a genuinely useful trick. When localStorage changes in one tab, the browser fires a storage event in every other tab of the same origin. That lets you keep multiple open tabs in sync β flip the theme in one, and the rest follow instantly.
// Runs in OTHER tabs when localStorage changes (never in the tab that wrote it)
window.addEventListener('storage', (event) => {
console.log('Key changed:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
if (event.key === 'theme') {
document.body.classList.toggle('dark', event.newValue === 'dark');
}
});
β οΈ Two things that surprise people
The event does not fire in the same tab that made the change β only in the others. And sessionStorage changes never fire it at all, because that data is private to a single tab. Use it for cross-tab localStorage sync only.
Security & Privacy
Web Storage is convenient, but it is not a vault. Data is stored unencrypted on disk and is readable by any JavaScript running on your origin. That has direct consequences for what belongs there.
β οΈ Never store these client-side
- Passwords or password hashes
- Long-lived authentication tokens (a stolen token = a stolen session). Prefer
HttpOnlycookies, which JavaScript cannot read. - Credit-card numbers or other financial data
- Personally identifying information (PII) you wouldn't want an XSS attacker to exfiltrate
β Good candidates for Web Storage
- UI preferences β theme, language, layout, "I've dismissed this banner"
- Non-sensitive drafts and in-progress form data
- Short-lived, non-critical caches of public API responses
- Anonymous, non-identifying app state
The core threat is XSS (cross-site scripting): if an attacker can inject script into your page, they can read everything in Web Storage. That's why an HttpOnly auth cookie beats a token in localStorage β the cookie is invisible to script. Whatever you do store, clear the sensitive parts on logout:
function logout() {
// Remove anything tied to the authenticated session
localStorage.removeItem('authToken');
localStorage.removeItem('userId');
sessionStorage.clear();
// Keep harmless preferences like theme if you like
window.location.href = '/login.html';
}
Finally, always feature-detect before relying on storage β some enterprise or privacy configurations disable it, and a bare access can throw:
function storageAvailable(type) {
try {
const s = window[type];
const t = '__test__';
s.setItem(t, t);
s.removeItem(t);
return true;
} catch {
return false;
}
}
if (storageAvailable('localStorage')) {
enablePersistentFeatures();
} else {
fallBackToInMemoryState();
}
Hands-on Exercise
ποΈ Build a "Remember My Preferences" Panel
Objective: Persist a small set of UI preferences with localStorage so they survive a page reload.
Instructions:
- Create a page with a theme
<select>(light/dark), a font-size<input type="range">, and a "Save" button. - On load, read a
prefsobject from storage (viaJSON.parse) and apply it to the controls and the<body>. - On Save, gather the current control values into one object and write it with
JSON.stringify. - Reload the page and confirm your choices stick. Then open DevTools β Application β Local Storage and watch the value change.
- Stretch: add a
storageevent listener so opening the page in a second tab and saving there updates the first tab live.
π‘ Hint
Store a single object under one key rather than many separate keys β it's easier to load and clear as a unit. Remember that the range input's .value is a string; use Number() if you do maths with it, or just keep it as a string for CSS.
β Sample solution
const KEY = 'prefs';
const themeSel = document.querySelector('#theme');
const sizeInp = document.querySelector('#size');
const saveBtn = document.querySelector('#save');
function apply(prefs) {
document.body.dataset.theme = prefs.theme;
document.body.style.fontSize = prefs.fontSize + 'px';
themeSel.value = prefs.theme;
sizeInp.value = prefs.fontSize;
}
function load() {
const raw = localStorage.getItem(KEY);
const prefs = raw ? JSON.parse(raw) : { theme: 'light', fontSize: 16 };
apply(prefs);
}
saveBtn.addEventListener('click', () => {
const prefs = { theme: themeSel.value, fontSize: Number(sizeInp.value) };
localStorage.setItem(KEY, JSON.stringify(prefs));
apply(prefs);
});
// Stretch: live cross-tab sync
window.addEventListener('storage', (e) => {
if (e.key === KEY && e.newValue) apply(JSON.parse(e.newValue));
});
load();
π― Quick Quiz
Question 1: You run localStorage.setItem('count', 5) then localStorage.getItem('count') === 5. What is the result?
Question 2: Which statement about sessionStorage is true?
Question 3: Why is a long-lived auth token safer in an HttpOnly cookie than in localStorage?
Summary & Quiz
π Key Takeaways
- Web Storage is a simple string key/value store scoped to one origin, with a ~5 MB budget.
- localStorage persists until deleted; sessionStorage is per-tab and dies with the tab β same API, different lifetime.
- Store objects with
JSON.stringify/JSON.parse, and remember to rehydrate dates manually. - Everything is stored as a string β convert numbers and booleans on the way out.
- The storage event syncs localStorage across tabs; it never fires for sessionStorage or in the writing tab.
- Never keep passwords, financial data, or long-lived tokens client-side; feature-detect and clear on logout.
π Further Reading
- MDN β Web Storage API
- javascript.info β LocalStorage, sessionStorage
- web.dev β Storage for the Web (when to use what)
- MDN β IndexedDB (for larger, structured data)
π What's Next?
Web Storage is just one of dozens of capabilities the browser exposes to JavaScript. Next we'll zoom out and survey the whole landscape of Browser APIs β from geolocation and the camera to background workers β so you know what tools are on the shelf.
π Nice work!
Your apps can now remember things. Let's see everything else the browser can do.