LocalStorage, sessionStorage
The browser gives you two objects for stashing key/value pairs on the user’s machine: localStorage and sessionStorage. Both live in JavaScript, both hold strings, and both survive things you might expect to wipe them out.
The headline feature is persistence. Data in sessionStorage lives through a page reload. Data in localStorage goes further — it outlasts a full browser restart, and even an OS reboot. Close everything, come back tomorrow, and the value is still there.
Cookies already store data on the client, so why add two more objects? Because web storage fixes three things cookies handle poorly:
- Nothing is sent to the server. Cookies ride along with every HTTP request to their domain, which bloats traffic and caps their practical size at ~4KB. Web storage never leaves the browser, so you can keep much more. Most browsers allow at least 5MB, often more, with user-configurable limits.
- The server can’t touch it. There’s no
Set-Storageheader. Everything happens in JavaScript on the page. The server has no way to read or overwrite these objects through HTTP. - It’s locked to an origin. Storage is bound to the origin — the protocol + domain + port triplet.
http://site.comandhttps://site.comget separate storage. So dosite.comandsub.site.com. Neither can read the other’s data.
Both objects share the exact same API:
setItem(key, value)— store a key/value pair.getItem(key)— read the value for a key.removeItem(key)— delete one key and its value.clear()— wipe everything.key(index)— get the key sitting at a given position.length— how many items are stored.
It reads like a Map — you get set/get/remove by key — with one extra trick: key(index) lets you reach items by position, which a Map doesn’t offer directly.
Here’s the whole API in one place. This sandbox can’t touch the real localStorage (it runs on a locked-down origin), so it uses a stand-in object with the identical methods — setItem, getItem, removeItem, clear, plus length and key(i). The table below is built the way the real thing forces you to: by walking indexes with key(i). Notice getItem on a missing key returns null.
localStorage demo
The two defining traits of localStorage:
- Shared across every tab and window on the same origin.
- Permanent — the data has no expiry. It survives a browser restart and an OS reboot until something explicitly removes it.
Run this once:
localStorage.setItem('test', 1);
Now close the browser and reopen it, or just open the same page in another window. The value is waiting:
alert( localStorage.getItem('test') ); // 1
You only need to be on the same origin (protocol/domain/port). The URL path can differ — /page-a and /page-b on the same site share one localStorage. Set a value in one window and it’s immediately visible in every other window on that origin.
Object-like access
Web storage predates the Map style API, and for backward compatibility you can treat it like a plain object — dot or bracket notation for get, set, and delete:
// set key
localStorage.test = 2;
// get key
alert( localStorage.test ); // 2
// remove key
delete localStorage.test;
It mostly works, but reach for setItem/getItem instead. Two reasons the object style bites:
-
Keys can collide with built-in names. If a key comes from user input, it might be anything — including
length,toString, or another real property of the storage object. The method API handles those fine; the object syntax breaks:let key = 'length'; localStorage[key] = 5; // Error, can't assign lengthHere
lengthis a read-only property of the storage object, so assigning to it throws.localStorage.setItem('length', 5)would have stored it as an ordinary item. -
No storage event fires. Writing through
setItemtriggers thestorageevent that lets other windows react. Assigning a property directly skips it — a detail covered later in this chapter.
Looping over keys
The methods give you get/set/remove by a single key. But how do you walk all the stored values?
Storage objects aren’t iterable — no for..of, no spread. So you loop by index, the way you’d walk an array:
for(let i=0; i<localStorage.length; i++) {
let key = localStorage.key(i);
alert(`${key}: ${localStorage.getItem(key)}`);
}
You might reach for for..in, since it works on regular objects. It does enumerate the keys — but it also drags in inherited method names you don’t want:
// bad try
for(let key in localStorage) {
alert(key); // shows getItem, setItem and other built-in stuff
}
for..in visits every enumerable property up the prototype chain, so getItem, setItem, and friends all show up alongside your real keys. Two ways to clean that up.
Filter each key with a hasOwnProperty check to skip the prototype’s members:
for(let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue; // skip keys like "setItem", "getItem" etc
}
alert(`${key}: ${localStorage.getItem(key)}`);
}
Or skip the mess entirely and grab only the own keys with Object.keys, then loop those:
let keys = Object.keys(localStorage);
for(let key of keys) {
alert(`${key}: ${localStorage.getItem(key)}`);
}
Object.keys returns only the object’s own keys and ignores the prototype, so the built-in methods never appear. That’s the cleanest option.
Strings only
Both the key and the value have to be strings. That’s the rule people trip over most.
Pass anything else — a number, an object — and it’s silently coerced to a string first:
localStorage.user = {name: "Maya"};
alert(localStorage.user); // [object Object]
The object never made it in. What got stored is "[object Object]", the result of calling String() on it. Your data is gone.
The fix is JSON: serialize on the way in, parse on the way out.
localStorage.user = JSON.stringify({name: "Maya"});
// sometime later
let user = JSON.parse( localStorage.user );
alert( user.name ); // Maya
Watch the difference for yourself. Storing the object directly runs it through String() first — you get back "[object Object]" and the fields are gone. Going through JSON.stringify/JSON.parse gives you a real object again:
You can also stringify the whole storage object at once, handy for a quick debugging dump:
// the extra arguments to JSON.stringify pretty-print with 2-space indentation
alert( JSON.stringify(localStorage, null, 2) );
sessionStorage
sessionStorage shows up far less often than localStorage. Same methods, same properties — but a much tighter scope.
- It exists only inside the current browser tab.
- Open the same page in a second tab and it gets its own, separate
sessionStorage. - It is shared with iframes inside the same tab, as long as they’re the same origin.
- Open the same page in a second tab and it gets its own, separate
- The data survives a page refresh, but not closing the tab. Close it, and the storage is gone.
Try it. Run this:
sessionStorage.setItem('test', 1);
Refresh the page. The value is still there:
alert( sessionStorage.getItem('test') ); // after refresh: 1
Now open the same page in a new tab and run the read again — you get null, because that tab has its own empty sessionStorage. The store is tied to both the origin and the individual tab, which is exactly why it’s used sparingly. It fits data that should belong to one visit in one tab: a multi-step form, a scroll position, a temporary draft.
Storage event
When data changes in localStorage or sessionStorage, a storage event fires. It carries:
key— the key that changed (nullwhen.clear()ran).oldValue— the previous value (nullif the key is brand new).newValue— the new value (nullif the key was removed).url— the URL of the document that made the change.storageArea— the storage object that was modified, eitherlocalStorageorsessionStorage.
Here’s the part that makes it useful: the event fires on every other window that can see that storage — but not on the one that caused the change. The window that writes doesn’t get notified; it already knows.
Picture two windows open on the same site. They share one localStorage. Open this page in two windows to try the code below yourself.
If both windows listen for window.onstorage, each reacts to writes made by the other:
// triggers on updates made to the same storage from other documents
window.onstorage = event => { // can also use window.addEventListener('storage', event => {
if (event.key != 'now') return;
alert(event.key + ':' + event.newValue + " at " + event.url);
};
localStorage.setItem('now', Date.now());
Notice event.url tells you which document made the write. And event.storageArea hands you the actual storage object that changed — the same event type serves both localStorage and sessionStorage, so this property points at whichever one was touched. You could even write a value straight back into it to “reply” to the change.
This turns web storage into a message bus between windows on the same origin. One window sets a key; every other window hears about it and can act.
Modern browsers also ship the Broadcast Channel API, a purpose-built channel for same-origin, cross-window messaging. It’s richer than piggybacking on storage, though a touch less universally supported. There are libraries that polyfill it on top of localStorage so you can use it everywhere.
Summary
localStorage and sessionStorage store key/value pairs in the browser, out of the server’s reach.
- Keys and values are always strings — use
JSON.stringify/JSON.parsefor anything else. - The limit is roughly 5MB, and varies by browser.
- The data doesn’t expire on its own.
- It’s scoped to the origin (protocol/domain/port).
localStorage |
sessionStorage |
|---|---|
| Shared across all tabs and windows on the same origin | Scoped to one browser tab, including same-origin iframes in it |
| Survives a browser restart | Survives a page refresh, but not closing the tab |
API:
setItem(key, value)— store a key/value pair.getItem(key)— read the value for a key.removeItem(key)— delete a key and its value.clear()— wipe everything.key(index)— get the key at positionindex.length— the number of stored items.- Use
Object.keysto list all your keys. - Accessing keys as object properties works, but skips the
storageevent.
Storage event:
- Fires on
setItem,removeItem, andclear. - Carries the full picture:
key,oldValue,newValue, the documenturl, and thestorageArea. - Fires on every window that can see the storage except the one that triggered it — within a tab for
sessionStorage, across all windows forlocalStorage.