πΎ Local Storage and Session Storage
Your browser ships with a tiny key-value database that survives page reloads β no server, no cookies, no libraries. In this lesson you'll learn to store and read data with the Web Storage API, decide when to reach for localStorage versus sessionStorage, and dodge the sharp edges: JSON serialization, quota limits, and the very real security risks.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between localStorage and sessionStorage and choose the right one for a task
- Use
setItem,getItem,removeItem, andclearto manage stored data - Serialize objects and arrays with
JSON.stringify/JSON.parseand add expiry logic - Synchronize state across browser tabs using the storage event
- Handle quota errors and identify what must never be stored client-side
Estimated Time: 35β45 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a form that auto-saves as you type and restores itself after a reload.
In This Lesson
What Is Web Storage?
The Web Storage API lets a website keep small amounts of data directly inside the user's browser, tied to your site's origin. Before it existed, developers stuffed everything into cookies β which are limited to about 4 KB, are sent to the server on every single request (wasting bandwidth), and require awkward string parsing.
Web Storage fixes all three problems: roughly 5 MB of room per origin, data that stays in the browser and is never automatically transmitted, and a dead-simple key/value interface.
π‘ A useful analogy:localStorageis a filing cabinet in your office β it's still there tomorrow when you come back.sessionStorageis a whiteboard in a meeting room β perfect for working notes, but wiped clean the moment you leave.
π Key Terms
Origin: the combination of scheme + host + port (e.g. https://shop.example.com). Storage is isolated per origin β a subdomain is a different origin.
Key/value store: data is saved as named strings; you look items up by their key, like words in a dictionary.
Serialization: converting a structured value (object, array) into a string so it can be stored, then back again.
localStorage vs sessionStorage
Both objects expose the exact same API. The only difference is lifetime and scope:
localStorage when data should outlive the tab, sessionStorage when it should not.How a "session" behaves
- Navigating within the same tab keeps sessionStorage.
- Reloading or restoring the page keeps it.
- Opening the same site in a new tab starts a fresh, empty sessionStorage.
- Duplicating a tab copies the current sessionStorage into the new one.
- Closing the tab destroys it.
| Feature | Cookies | Web Storage |
|---|---|---|
| Capacity | ~4 KB | ~5 MB |
| Sent with every request? | Yes (adds latency) | No (stays in browser) |
| API | Manual string parsing | Simple key/value methods |
| Expiration | Configurable | None or session-based |
The Core API
Five methods and one property cover everything. Every value is stored and returned as a string β remember that, because it trips up almost everyone at least once.
// Store values (both arguments are coerced to strings)
localStorage.setItem('username', 'ada_lovelace');
localStorage.setItem('visits', '1');
// Read a value back β returns null if the key is missing
const name = localStorage.getItem('username'); // 'ada_lovelace'
const missing = localStorage.getItem('nope'); // null
// Remove one key, or wipe everything for this origin
localStorage.removeItem('visits');
// localStorage.clear();
// Inspect the store
console.log(localStorage.length); // number of keys
console.log(localStorage.key(0)); // the key at index 0
β οΈ Everything is a string
Numbers, booleans, and objects are all coerced to text. localStorage.setItem('n', 5) stores the string "5", and getItem('n') returns "5", not 5. Convert on the way out: Number(localStorage.getItem('n')).
You can also use property syntax (localStorage.username = 'ada'), but prefer the methods β they read more clearly and avoid clashing with built-in property names like length or clear.
Storing Objects & Arrays
Because storage only holds strings, structured data must be run through JSON on the way in and out.
const prefs = {
theme: 'dark',
fontSize: 16,
notifications: { email: true, push: false }
};
// Serialize to a string before storing
localStorage.setItem('prefs', JSON.stringify(prefs));
// Parse back into an object when reading
const saved = JSON.parse(localStorage.getItem('prefs'));
console.log(saved.notifications.email); // true
A robust read guards against a missing or corrupted value so one bad entry can't crash your app:
function readJSON(key, fallback = null) {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
console.warn(`Corrupt JSON in "${key}" β using fallback`);
return fallback;
}
}
const searches = readJSON('recentSearches', []); // always an array
β οΈ JSON loses some types
Date objects become ISO strings, undefined and functions are dropped, and NaN/Infinity become null. If you need dates back as real Date objects, re-hydrate them yourself: new Date(saved.createdAt).
Cross-Tab Sync & Expiry
The storage event
When localStorage changes in one tab, every other tab on the same origin fires a storage event. (The tab that made the change does not receive it.) This gives you free cross-tab synchronization β change a setting in one tab and watch the others update live.
window.addEventListener('storage', (event) => {
if (event.key === 'prefs') {
console.log('Prefs changed in another tab');
console.log('old:', event.oldValue, 'new:', event.newValue);
applyPreferences(JSON.parse(event.newValue));
}
});
Adding your own expiry
Unlike cookies, Web Storage has no built-in expiration. You can bolt it on by storing a timestamp alongside the value:
function setWithExpiry(key, value, ttlMs) {
const record = { value, expiresAt: Date.now() + ttlMs };
localStorage.setItem(key, JSON.stringify(record));
}
function getWithExpiry(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const { value, expiresAt } = JSON.parse(raw);
if (Date.now() > expiresAt) {
localStorage.removeItem(key); // lazily evict on read
return null;
}
return value;
}
// Cache a result for 10 minutes
setWithExpiry('rates', { usd: 1.0 }, 10 * 60 * 1000);
π‘ Modern alternative: BroadcastChannel
For deliberate tab-to-tab messaging (not just storage changes), the BroadcastChannel API is cleaner: const ch = new BroadcastChannel('app'); ch.postMessage(data);. Reach for the storage event when the source of truth is the stored data.
Limits & Security
Quota handling
When you exceed the ~5 MB limit, setItem throws a QuotaExceededError. Never let that crash a save β wrap it:
function safeSet(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.warn('Storage full β evicting old data');
evictOldEntries();
return false;
}
throw err; // some other, unexpected error
}
}
π Never store secrets client-side
Web Storage is plaintext and readable by any JavaScript running on your page β including code injected by a cross-site scripting (XSS) attack. Do not put these here:
- Authentication tokens or session IDs (an XSS bug becomes full account takeover)
- Passwords or API keys
- Personal identifiable information (PII) or payment details
For auth tokens, prefer an HttpOnly, Secure, SameSite cookie the browser sends automatically but JavaScript cannot read.
Also note: some browsers block or wipe storage in private/incognito mode, and Safari may throw the moment you call setItem. Feature-detect before relying on it:
function storageAvailable() {
try {
const t = '__test__';
localStorage.setItem(t, t);
localStorage.removeItem(t);
return true;
} catch {
return false; // disabled, full, or private mode
}
}
Hands-on: Auto-Saving Form
ποΈ Build a form that never loses your work
Objective: Save form input to localStorage as the user types, restore it on reload, and clear it on successful submit.
Starter HTML
<form id="application">
<input name="fullName" placeholder="Full name">
<input name="email" type="email" placeholder="Email">
<textarea name="coverLetter" placeholder="Cover letter"></textarea>
<button type="submit">Submit</button>
</form>
<p id="status" aria-live="polite"></p>
Your tasks
- On every
inputevent, collect all field values into an object and save it under one key. - On page load, read that key and repopulate the fields.
- On
submit, remove the saved key so a fresh visit starts blank. - Bonus: debounce the save so it runs at most once every 400 ms.
π‘ Hint
Gather values with new FormData(form) then Object.fromEntries(...). To repopulate, loop the saved object and set form.elements[name].value. A debounce is just a setTimeout you clearTimeout on each keystroke.
β Solution
const form = document.getElementById('application');
const status = document.getElementById('status');
const KEY = 'applicationDraft';
// 1. Restore on load
const draft = JSON.parse(localStorage.getItem(KEY) || '{}');
for (const [name, value] of Object.entries(draft)) {
if (form.elements[name]) form.elements[name].value = value;
}
// 2. Save on input (debounced)
let timer;
form.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => {
const data = Object.fromEntries(new FormData(form));
localStorage.setItem(KEY, JSON.stringify(data));
status.textContent = 'Draft saved ' + new Date().toLocaleTimeString();
}, 400);
});
// 3. Clear on submit
form.addEventListener('submit', (e) => {
e.preventDefault();
localStorage.removeItem(KEY);
status.textContent = 'Submitted β draft cleared';
form.reset();
});
Open the page, type a few fields, reload β your text is still there. Submit, reload β it's gone. That's the whole feature in ~25 lines.
Best Practices
β Do
- Wrap every
JSON.parsein a try/catch and provide a fallback value. - Namespace your keys (
myapp:prefs) so different features don't collide. - Feature-detect availability before depending on storage.
- Use
sessionStoragefor anything that shouldn't leak between visits.
π« Don't
- Store tokens, passwords, or PII β treat storage as public to your own scripts.
- Assume values are anything but strings.
- Use it as a real database β for large or queryable data, use IndexedDB.
- Forget that quota exists; guard
setItemagainstQuotaExceededError.
Summary & Quiz
π Key Takeaways
- localStorage persists forever and is shared across tabs; sessionStorage lives only for one tab's session.
- The API is five methods β
setItem,getItem,removeItem,clear,keyβ and everything is stored as a string. - Use
JSON.stringify/JSON.parsefor objects, and defend parsing with try/catch. - The storage event syncs data across tabs for free.
- It's plaintext and XSS-readable β never store secrets, and guard against quota errors.
π― Quick Quiz
Question 1: A user fills out a multi-step wizard and you want the progress gone the moment they close the tab. Which store fits best?
Question 2: You run localStorage.setItem('count', 5) then localStorage.getItem('count'). What comes back?
Question 3: Why is storing a login token in localStorage risky?
π Further Reading
π What's Next?
Next we leave the filing cabinet behind and ask the browser a very different question: where are you? The Geolocation API lets your app read the user's coordinates β with their permission β to power maps, store finders, and weather.
π Nice work!
You can now persist state entirely in the browser. Your forms will never lose a user's work again.