Skip to content
Happy Programming Guide
Start learning
JavaScript

Working with Browser Storage in JavaScript

localStorage, sessionStorage, cookies and IndexedDB compared — what each is for, the quota and JSON traps, and what should never go in any of them.

An open laptop on a wooden desk

Browsers give you four places to keep data on the visitor’s device: localStorage, sessionStorage, cookies and IndexedDB. They differ in size, lifetime, and whether the server sees them. Choosing correctly is mostly a matter of answering two questions — how long should this last, and does the server need it?

Choosing between them#

localStorage sessionStorage Cookies IndexedDB
Size about 5 MB about 5 MB 4 KB hundreds of MB
Lifetime until cleared until tab closes you choose until cleared
Sent to the server No No Every request No
Stores strings strings strings objects, files
API synchronous synchronous synchronous asynchronous
Good for preferences, drafts one-visit state auth, server needs offline data

localStorage#

JavaScript
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");     // "dark"
localStorage.removeItem("theme");
localStorage.clear();                            // everything for this origin

localStorage.getItem("missing");                 // null, not undefined
console.log(localStorage.length);

Everything is a string. That is the source of most bugs with it:

JavaScript
localStorage.setItem("count", 5);
const count = localStorage.getItem("count");
console.log(count + 1);          // "51" - string concatenation
console.log(Number(count) + 1);  // 6

localStorage.setItem("enabled", false);
if (localStorage.getItem("enabled")) {
  console.log("this runs");      // "false" is a non-empty string
}

Objects need JSON#

JavaScript
const settings = { theme: "dark", fontSize: 16 };

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

const stored = JSON.parse(localStorage.getItem("settings"));
console.log(stored.theme);

Forget the stringify and you store the text [object Object], which parses back to nothing useful.

Wrap both directions, because parsing can throw:

JavaScript
function save(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch (error) {
    console.warn("Could not save:", error.name);
    return false;
  }
}

function load(key, fallback = null) {
  try {
    const raw = localStorage.getItem(key);
    return raw === null ? fallback : JSON.parse(raw);
  } catch {
    return fallback;      // corrupt or hand-edited value
  }
}

Note what JSON does not survive: Date objects become strings, undefined and functions disappear, and Map and Set become empty objects. Convert them yourself before saving.

When storage is unavailable#

JavaScript
function storageAvailable() {
  try {
    const key = "__test__";
    localStorage.setItem(key, "1");
    localStorage.removeItem(key);
    return true;
  } catch {
    return false;
  }
}

Reading or writing can throw, not just return null: some browsers block site data entirely, private windows may refuse writes, and the quota can be full. A page that assumes storage works breaks completely for those visitors.

JavaScript
try {
  localStorage.setItem("big", hugeString);
} catch (error) {
  if (error.name === "QuotaExceededError") {
    // clear old entries and retry, or fall back to memory
  }
}

sessionStorage#

JavaScript
sessionStorage.setItem("step", "3");

Identical API, different lifetime: it is cleared when the tab closes, and each tab has its own copy. Right for the current step of a multi-page form, a scroll position, or anything that should not persist to the next visit.

Reacting to changes in other tabs#

JavaScript
window.addEventListener("storage", (event) => {
  if (event.key === "theme") {
    applyTheme(event.newValue);
  }
});

The event fires in other tabs on the same origin, not the one that made the change. It is a simple way to keep several open tabs in sync — a theme toggle, or signing out everywhere at once.

Cookies#

JavaScript
document.cookie = "theme=dark; path=/; max-age=31536000; SameSite=Lax; Secure";

function getCookie(name) {
  return document.cookie
    .split("; ")
    .find(row => row.startsWith(name + "="))
    ?.split("=")[1];
}

// Delete by expiring it
document.cookie = "theme=; path=/; max-age=0";

The flags matter more than the syntax:

  • Secure — only sent over HTTPS.
  • HttpOnly — not readable from JavaScript. Set by the server; essential for session cookies.
  • SameSite=Lax or Strict — limits sending on cross-site requests, which blocks most CSRF.
  • Max-Age or Expires — without one, the cookie disappears when the browser closes.

Use cookies when the server needs the value on every request. For anything client-only, localStorage is better — cookies add weight to every single request you make.

IndexedDB#

Terminal
npm install idb
JavaScript
import { openDB } from "idb";

const db = await openDB("notes-db", 1, {
  upgrade(db) {
    db.createObjectStore("notes", { keyPath: "id" });
  },
});

await db.put("notes", { id: 1, text: "Hello", created: new Date() });
const note = await db.get("notes", 1);
const all = await db.getAll("notes");

The raw IndexedDB API is famously awkward, so a small wrapper is worth it. Reach for IndexedDB when you need more than a few megabytes, structured querying, or to store files and images — offline applications, in short.

What must never go in browser storage#

  • Passwords, in any form.
  • Payment details — card numbers, security codes.
  • Anything from which access can be derived — API secrets, signing keys.
  • Personal data you do not need on the device.

A practical example#

JavaScript
const form = document.querySelector("#contact");
const KEY = "contact-draft";

// Restore on load
const draft = load(KEY, {});
for (const [name, value] of Object.entries(draft)) {
  const field = form.elements[name];
  if (field) field.value = value;
}

// Save as they type
form.addEventListener("input", () => {
  const data = Object.fromEntries(new FormData(form));
  save(KEY, data);
});

// Clear on successful submit
form.addEventListener("submit", () => localStorage.removeItem(KEY));

Twelve lines that stop someone losing a long message to an accidental refresh — probably the single best return on effort in this whole area.

Questions people ask#

Does localStorage have an expiry?

No. Store a timestamp alongside the value and check it yourself if you need one.

Is it shared between subdomains?

No. Storage is per origin, so scheme, host and port must all match. Cookies can be shared across subdomains with the Domain attribute.

Do I need a cookie banner for localStorage?

It depends on what you store and where your users are. Necessary functional storage is generally treated differently from tracking. Take proper advice for anything commercial — the technology is not what decides it.

Why is my stored value null after a redirect?

Most likely a different origin — http versus https, or www versus bare domain. Print location.origin on both pages and compare.

Where to go next#

JavaScript local storage, in depthRead next

Keep reading

JavaScript

JavaScript Basics

The syntax of JavaScript in one page: variables, types, operators, functions, conditions, loops, arrays, objects and the mistakes beginners make with each.

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *