JavaScriptlocalStorage & sessionStorage

Web Storage: localStorage and sessionStorage

Web Storage gives every browser tab two key/value stores accessible from JavaScript: localStorage (persists across reloads and tab restarts) and sessionStorage (lives only as long as the tab is open). Both are synchronous, string-only, and scoped to the origin — the protocol + host + port combination. They are simple, fast and ubiquitous; just don't reach for them when you have a lot of data.

The five methods

JS
localStorage.setItem("theme", "dark");
localStorage.getItem("theme");        // "dark"
localStorage.removeItem("theme");
localStorage.clear();                  // wipe the whole store

localStorage.length;                   // number of keys
localStorage.key(0);                   // the first key (insertion order is not guaranteed)

sessionStorage has exactly the same API — different lifetime.

localStorage vs sessionStorage
  • localStorage — persists indefinitely until cleared by user, by your code, or by browser eviction. Shared between tabs of the same origin.

  • sessionStorage — wiped when the tab is closed. Not shared between tabs, even of the same origin.

  • Both are per-origin: a script on https://app.example.com cannot read storage from https://api.example.com.

  • Both are synchronous and block the main thread while reading/writing.

It is strings all the way down

The store can only hold strings. Anything else is coerced via String(value), which loses structure.

JS
localStorage.setItem("count", 7);
console.log(localStorage.getItem("count"));        // "7"  ← string, not number
console.log(typeof localStorage.getItem("count")); // "string"

localStorage.setItem("user", { name: "Ada" });
console.log(localStorage.getItem("user"));         // "[object Object]"  ← junk
7
string
[object Object]
JSON round-trip for objects

storage-helpers.js

JS
function save(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

function load(key, fallback = null) {
  const raw = localStorage.getItem(key);
  if (raw === null) return fallback;
  try {
    return JSON.parse(raw);
  } catch {
    return fallback;
  }
}

save("user", { name: "Ada", roles: ["admin"] });
load("user");      // { name: "Ada", roles: ["admin"] }
load("missing", { name: "Guest" }); // { name: "Guest" }
Note
JSON cannot represent `Date`, `Map`, `Set`, functions, `undefined` or circular references. Convert to a string representation yourself if you need those.
Capacity and quota

The spec only requires "at least 5 MB", and browsers vary:

  • Chromium browsers: roughly 5–10 MB per origin.

  • Firefox: 10 MB per origin (often shared across eTLD+1).

  • Safari: 5 MB per origin; cleared after ~7 days of inactivity by ITP.

  • Mobile Safari and in-app webviews are the most aggressive about evicting storage.

Handle quota errors

JS
function tryWrite(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (err) {
    if (err.name === "QuotaExceededError") {
      console.warn("localStorage is full");
      return false;
    }
    throw err;
  }
}
Warning
Storage is also blocked or throws in private/incognito mode and in some webviews. Always wrap writes in try/catch if losing the write would break your UI.
The storage event: syncing across tabs

When localStorage changes in one tab, every other tab of the same origin receives a storage event. The originating tab does not. This is the easiest way to keep multiple tabs in sync.

JS
addEventListener("storage", (e) => {
  if (e.key === "theme") {
    document.documentElement.dataset.theme = e.newValue;
  }
  // e.key, e.oldValue, e.newValue, e.url, e.storageArea
});

// In another tab:
localStorage.setItem("theme", "dark");

sessionStorage does not fire storage events between tabs (because each tab has its own session store).

What not to store
  • Auth tokens or anything secret. Any script on the page — including a compromised npm dependency — can read localStorage. Prefer HTTP-only cookies for session tokens.

  • Large binary data. Anything bigger than a few hundred KB belongs in IndexedDB.

  • Data you need on the server. Storage stays in the browser; the server never sees it.

  • Sensitive user data. It survives across sessions and may be backed up by the OS.

A typical real-world use

Remember user preferences

JS
const Theme = {
  get() {
    return localStorage.getItem("theme") || "system";
  },
  set(value) {
    localStorage.setItem("theme", value);
    document.documentElement.dataset.theme = value;
  },
};

// On boot:
document.documentElement.dataset.theme = Theme.get();

// On click:
document.querySelector("#dark").addEventListener("click", () => {
  Theme.set("dark");
});
When to reach for something else
Use `localStorage`/`sessionStorage` for small key-value preferences and tiny caches. Use **IndexedDB** for structured data, offline records and anything > 1 MB. Use **cookies** when the server needs to read the value on each request.