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-Storage header. 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.com and https://site.com get separate storage. So do site.com and sub.site.com. Neither can read the other’s data.
cookies
~4KB · sent to server every request · server can set via header
localStorage
~5MB+ · stays in browser · shared across tabs · no expiry
sessionStorage
~5MB+ · stays in browser · per-tab · cleared on tab close
Three client-side stores, three scopes. Only cookies travel to the server.

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.

interactiveThe Storage API: set, get, remove, and walk by index

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.

Tab A
Tab B
Window C
↓   all read/write the same   ↓

localStorage for https://example.com

One localStorage bucket per origin, shared by all its tabs and windows.

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:

  1. 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 length

    Here length is 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.

  2. No storage event fires. Writing through setItem triggers the storage event 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.

your keys
“user”
“theme”
prototype (Storage)
getItem, setItem,
removeItem, clear, key…
reaches
index loop → your keys
Object.keys → your keys
for..in → both boxes
for..in walks the whole prototype chain; the index loop and Object.keys stay on your data.

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
{ name: “Maya” }
— JSON.stringify →
‘{“name”:“Maya”}’
→ localStorage →
JSON.parse →
{ name: “Maya” }
Objects can't live in storage directly — stringify going in, parse coming out.

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:

interactiveObjects in storage: coerced to text vs. JSON round-trip

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.
  • 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.

localStorage
Tab 1
Tab 2
↓ share ↓
one bucket
sessionStorage
Tab 1 → bucket A
Tab 2 → bucket B
separate, not shared
localStorage is one bucket per origin; sessionStorage is one bucket per tab.

Storage event

When data changes in localStorage or sessionStorage, a storage event fires. It carries:

  • key — the key that changed (null when .clear() ran).
  • oldValue — the previous value (null if the key is brand new).
  • newValue — the new value (null if the key was removed).
  • url — the URL of the document that made the change.
  • storageArea — the storage object that was modified, either localStorage or sessionStorage.

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());
Window A
— setItem(‘now’, …) →
localStorage
Window B
← storage event (key, newValue, url) —
localStorage
Window A does not receive its own event.
Window A writes; the storage event lands on Window B, never back on A.

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.parse for 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 position index.
  • length — the number of stored items.
  • Use Object.keys to list all your keys.
  • Accessing keys as object properties works, but skips the storage event.

Storage event:

  • Fires on setItem, removeItem, and clear.
  • Carries the full picture: key, oldValue, newValue, the document url, and the storageArea.
  • Fires on every window that can see the storage except the one that triggered it — within a tab for sessionStorage, across all windows for localStorage.