JavaScript by Example

localStorage and sessionStorage

Browser key-value storage that survives page reloads - what it stores, what it doesn't, and why auth tokens and PII must never go near it.

localStorage and sessionStorage are key-value stores built into every browser. They let you persist data across page loads without a server. Both accept only strings as keys and values, and both are scoped to the current origin (the combination of protocol, hostname, and port). The difference: localStorage persists until you clear it; sessionStorage is wiped when the tab closes. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.

The API is four methods: setItem to write, getItem to read, removeItem to delete one key, and clear to delete everything in that storage for the origin. Because both APIs only store strings, a number or boolean you write comes back as a string.

// Write
localStorage.setItem("theme", "dark");
localStorage.setItem("fontSize", "16");
 
// Read
console.log(localStorage.getItem("theme"));    // "dark"
console.log(localStorage.getItem("fontSize")); // "16" (string, not number)
console.log(localStorage.getItem("missing"));  // null
 
// Delete one key
localStorage.removeItem("fontSize");
 
// Delete everything for this origin
localStorage.clear();
 
// sessionStorage has the identical API but only lasts for the tab session
sessionStorage.setItem("draftTitle", "Untitled post");
console.log(sessionStorage.getItem("draftTitle")); // "Untitled post"
// Cleared automatically when the tab closes

To store objects or arrays, serialize them to a JSON string with JSON.stringify before writing, and deserialize with JSON.parse after reading. JSON.parse throws if the stored string is not valid JSON - which can happen if a previous version of your app wrote something else - so always wrap it in try/catch.

const settings = { theme: "dark", fontSize: 16, compact: true };
 
// Write: serialize to JSON first
localStorage.setItem("settings", JSON.stringify(settings));
 
// Read: parse back to an object
function loadSettings() {
  try {
    const raw = localStorage.getItem("settings");
    return raw ? JSON.parse(raw) : null;
  } catch {
    // The stored string was not valid JSON - fall back to a default
    return null;
  }
}
 
const loaded = loadSettings();
console.log(loaded?.theme); // "dark"
console.log(typeof loaded?.fontSize); // "number" (JSON.parse restores the type)

The storage event fires on all other tabs and windows that share the same origin whenever localStorage changes. This lets separate tabs stay in sync without a server connection. The event does not fire in the tab that made the change.

// In Tab A: listen for changes made by other tabs
window.addEventListener("storage", (event) => {
  console.log("key changed:", event.key);
  console.log("old value:", event.oldValue);
  console.log("new value:", event.newValue);
 
  if (event.key === "theme") {
    document.body.dataset.theme = event.newValue ?? "light";
  }
});
 
// In Tab B: making this change fires the event in Tab A (not in Tab B itself)
localStorage.setItem("theme", "light");

In production

Both APIs are synchronous and run on the main thread - large reads and writes show up as long tasks in a performance profile. Quota is around 5 MB per origin; exceeding it throws a QuotaExceededError. Any JavaScript on the page can read everything in localStorage, so never store auth tokens, session cookies, or personally identifiable information here. Use HttpOnly cookies for secrets - the browser keeps them out of JavaScript entirely.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free