Skip to content
JavaScript js browser 5 min read

localStorage & sessionStorage

The Web Storage API gives every page a simple key-value store that lives in the browser, no server round-trip required. It comes in two flavors — localStorage, which persists until explicitly cleared, and sessionStorage, which lasts only for the lifetime of a tab. Both share the same tiny API, store strings only, and are perfect for remembering user preferences, caching small payloads, or holding draft form data. Understanding their limits and quirks keeps you from reaching for cookies or a database when a few lines of storage will do.

The core API

Both localStorage and sessionStorage implement the same Storage interface, exposed as global objects on window. You read and write through five methods plus a length property.

MemberDescription
setItem(key, value)Stores value under key. Both are coerced to strings.
getItem(key)Returns the string for key, or null if it doesn’t exist.
removeItem(key)Deletes the entry for key.
clear()Removes every entry in that origin’s store.
key(index)Returns the key name at the given numeric index.
lengthThe number of stored entries (a property, not a method).
localStorage.setItem("theme", "dark");

const theme = localStorage.getItem("theme");
console.log(theme);            // "dark"
console.log(localStorage.getItem("missing")); // null

localStorage.removeItem("theme");
console.log(localStorage.length); // 0

Output:

dark
null
0

Missing keys return null, never undefined. Always handle the null case before parsing or rendering a stored value.

localStorage vs sessionStorage

The two stores behave identically at the API level; the difference is lifetime and scope. localStorage survives page reloads, tab closes, and browser restarts. sessionStorage is scoped to a single tab and is wiped the moment that tab closes — opening the same site in a new tab starts with an empty sessionStorage.

AspectlocalStoragesessionStorage
LifetimeUntil explicitly clearedUntil the tab/window closes
Shared across tabsYes (same origin)No — isolated per tab
Survives reloadYesYes
Survives browser restartYesNo
Sent to serverNeverNever

Use localStorage for long-lived preferences (theme, language, “remember me”). Reach for sessionStorage for transient state that should not leak between tabs, like a multi-step wizard or a one-time-use token.

Storing objects with JSON

Storage only holds strings. Anything else is coerced via String(), so an object becomes the useless "[object Object]". To persist structured data, serialize it with JSON.stringify on the way in and JSON.parse on the way out.

const user = { id: 7, name: "Ada", roles: ["admin"] };

localStorage.setItem("user", JSON.stringify(user));

const raw = localStorage.getItem("user");
const restored = raw ? JSON.parse(raw) : null;

console.log(restored.name, restored.roles[0]);

Output:

Ada admin

A small wrapper makes this safe and reusable, guarding against missing keys and malformed JSON:

const store = {
  get(key, fallback = null) {
    const raw = localStorage.getItem(key);
    if (raw === null) return fallback;
    try {
      return JSON.parse(raw);
    } catch {
      return fallback;
    }
  },
  set(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },
};

store.set("cart", [{ sku: "A1", qty: 2 }]);
console.log(store.get("cart"));        // [ { sku: "A1", qty: 2 } ]
console.log(store.get("none", []));    // []

Limits and constraints

Web Storage is intentionally small and synchronous. Keep these boundaries in mind:

  • ~5 MB per origin in most browsers (some allow up to 10 MB). Exceeding the quota throws a QuotaExceededError.
  • Strings only — numbers, booleans, dates, and objects must be serialized.
  • Synchronous — every read and write blocks the main thread, so avoid storing large blobs.
  • Same-originhttps://a.com and https://b.com (and http vs https) have separate, isolated stores.
try {
  localStorage.setItem("big", "x".repeat(10 * 1024 * 1024));
} catch (err) {
  if (err.name === "QuotaExceededError") {
    console.log("Storage full — evict something first");
  }
}

Output:

Storage full — evict something first

A live counter demo

This self-contained pen reads a count from localStorage on load, increments it on each click, and persists it — refresh the result pane and the value sticks.

<button id="inc">Clicks: 0</button>
<button id="reset">Reset</button>
<script>
  const btn = document.getElementById("inc");
  const reset = document.getElementById("reset");

  const render = () => {
    const n = Number(localStorage.getItem("clicks") || 0);
    btn.textContent = `Clicks: ${n}`;
  };

  btn.addEventListener("click", () => {
    const n = Number(localStorage.getItem("clicks") || 0) + 1;
    localStorage.setItem("clicks", String(n));
    render();
  });

  reset.addEventListener("click", () => {
    localStorage.removeItem("clicks");
    render();
  });

  render();
</script>

Reacting to the storage event

When localStorage changes, the browser fires a storage event on every other tab of the same origin — but not on the tab that made the change. This is the built-in way to keep multiple tabs in sync, for example logging the user out everywhere when one tab signs out.

window.addEventListener("storage", (event) => {
  console.log(event.key, event.oldValue, "->", event.newValue);
});

The StorageEvent carries key, oldValue, newValue, url, and the affected storageArea. Note that sessionStorage changes do not propagate this way, since each tab has its own session store.

The demo below logs whenever storage changes. Open the rendered page in two browser tabs, type in one, and watch the other update.

<input id="msg" placeholder="Type to broadcast across tabs" />
<pre id="log">Open this pen in two tabs…</pre>
<script>
  const input = document.getElementById("msg");
  const log = document.getElementById("log");

  input.addEventListener("input", () => {
    localStorage.setItem("shared-msg", input.value);
  });

  window.addEventListener("storage", (event) => {
    if (event.key === "shared-msg") {
      log.textContent = `Other tab says: ${event.newValue}`;
    }
  });
</script>

Best Practices

  • Always namespace keys (e.g. "app:theme") to avoid collisions with other scripts on the same origin.
  • Treat every getItem result as possibly null and validate before parsing.
  • Wrap JSON.parse in try/catch — stored data can be corrupted or hand-edited.
  • Wrap setItem in try/catch to handle QuotaExceededError gracefully.
  • Never store secrets, tokens, or sensitive data — Web Storage is readable by any script on the page (XSS-exposed).
  • Prefer sessionStorage for per-tab transient state and localStorage for durable preferences.
  • Use the storage event to keep multiple tabs consistent instead of polling.
Last updated June 1, 2026
Was this helpful?