JS localStorage Table

PieceWhat it doesField note
localStorage.setItem(k, v)Persist stringsStrings ONLY - objects go through JSON (see the pairing below)
localStorage.getItem(k)Read (null if absent)Missing key = null, not undefined - the falsy check that misleads
localStorage.removeItem(k)Delete oneNo return value - idempotent on absent keys
storage eventCross-tab notifyFires in OTHER tabs, not the writer - free multi-tab sync
sessionStorageTab-scoped twinDies with the tab - drafts, wizards, per-tab state
5MB quotaStrings as UTF-16QuotaExceededError on overflow - try/catch every setItem
vs cookiesNo expiry, no auto-sendCookies ride every HTTP request; storage never leaves the browser
vs IndexedDBSync + tiny vs async + bigStructured data and files go to IDB - storage is the key-value pocket
Reference: the MDN localStorage reference. Web storage is a synchronous string dictionary per origin: setItem/getItem/removeItem, about 5MB, surviving reloads and browser restarts. The three surprises: values are STRINGS ONLY (objects ride JSON.stringify), the storage event fires in every OTHER tab but not the writer - which is free multi-tab synchronization - and quota overflow throws, so setItem belongs in try/catch in production. Bottom line: storage never auto-sends to the server (that is cookies' job), sessionStorage is the per-tab pocket, and anything structured or big graduates to IndexedDB. Related tools: JSON table (the string boundary both ways), cookies table (the server-side counterpart), and error table (the QuotaExceeded family).

Web storage is a synchronous string dictionary per origin: localStorage persists across reloads and browser restarts, sessionStorage dies with the tab, and both expose the same three methods - setItem, getItem, removeItem. Values are STRINGS ONLY; objects ride JSON.stringify on the way in and JSON.parse on the way out.

Bottom line: the storage event fires in every OTHER tab but never the writer - which makes cross-tab synchronization a one-liner (write in one tab, listen everywhere else). Quota overflow throws QuotaExceededError, so production setItem calls belong in try/catch, not just for politeness.

The honest part: storage never leaves the browser - no expiry, no automatic header. Cookies exist to ride HTTP requests; localStorage exists to stay local. And the 5MB quota counts strings as UTF-16, so non-ASCII data reaches the ceiling at half the character count you might expect.

How to use

  1. Persist state safely: localStorage.setItem('theme', JSON.stringify(theme)) with a matching JSON.parse(getItem(...)) - and try/catch the setItem for quota.
  2. Sync tabs for free: window.addEventListener('storage', e => applyTheme(e.newValue)) in every tab - the event carries key, oldValue and newValue.
  3. Scope drafts to the tab: sessionStorage for unsent form state and wizard steps - closing the tab is the cleanup you did not have to write.

Frequently asked questions

Why does my localStorage value come back as a string like "[object Object]"?

Because setItem stringifies its argument with the default String() conversion: storing an object directly produces the useless text '[object Object]'. The storage API's contract is strings only, so the object roundtrip is explicit: JSON.stringify before setItem, JSON.parse after getItem. The related trap: getItem returns null for MISSING keys but the string 'null' for a stored null - and JSON.parse(null) returns null by coercion luck, while JSON.parse(undefined) throws. Guard with an explicit null check before parsing anything from storage.

How does the storage event enable cross-tab synchronization?

When one tab writes to localStorage, every OTHER tab on the same origin receives a storage event carrying key, oldValue, newValue and url - the writing tab itself does NOT receive it. That asymmetry is the feature: a settings tab changes the theme, every open tab hears the event and re-renders without polling or a server. Two caveats: the event fires only when the value actually CHANGES (writing the same value is silent), and it does not fire for sessionStorage, which is deliberately tab-private.

When is sessionStorage the right choice over localStorage?

When the state's lifetime IS the tab. Draft form content the user has not sent, multi-step wizard progress, a per-tab filter or scroll position, A/B variant assignment that must stay consistent within one visit but may differ across visits - all of these want to vanish when the tab closes, and sessionStorage makes closing the tab the cleanup. localStorage would leak that state into the next session (stale drafts resurfacing days later). Both share the same API surface, so switching between them is a one-word change.

What actually separates localStorage from cookies and IndexedDB?

Direction and capacity. Cookies are small (4KB), expire on schedule, and ride EVERY HTTP request to the server - which is exactly what sessions need and exactly the overhead static data should not pay. localStorage is ~5MB, never expires, and never leaves the browser - perfect for preferences, wrong for anything the server must see per-request. IndexedDB is the async, structured, hundreds-of-MB database for files and large datasets, with a cursor API instead of key-value simplicity. The ladder: preferences to storage, server state to cookies (or better, Authorization headers), bulk data to IndexedDB.

Related tools