Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~23 min· topic 3 of 8

Topic 11.3

localStorage and sessionStorage

In one line

localStorage keeps small string key–value pairs for an origin across visits, sessionStorage keeps them for one tab; both store only strings (so use JSON), hold about 5 MB, block while they work, and must never hold secrets, because any script on the page can read them.

Think of it like this

A notebook left at the shop counter. Every time you visit the same shop (the same origin), the notebook is still there with what you wrote last time: that's localStorage. A sticky note on your table lasts only while you're sitting at that table, and goes in the bin when you leave: that's sessionStorage. Anyone working behind the counter can read the notebook, so you'd never write your bank PIN in it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Web Storage
The browser's simple key–value storage for a page: localStorage and sessionStorage.
Key and value
A name (key) and the text stored under it (value), like a label on a box and what's inside.
localStorage
Storage for an origin that lasts across visits until it is cleared.
sessionStorage
Storage for one browser tab that is deleted when the tab closes.
Serialise
Turn a value into text so it can be stored or sent; JSON.stringify does this.
Quota
The maximum amount a site may store. Going over it makes setItem throw QuotaExceededError.
Synchronous
The code waits right there until the work is done, blocking everything else on the page.
IndexedDB
A bigger, asynchronous database built into the browser, for lots of structured data.
HttpOnly cookie
A cookie the server sets that JavaScript cannot read, which keeps login sessions away from injected scripts.

Step by step

01Save and read a string

The simplest use: remember one setting. getItem gives back exactly the string you stored, or null if nothing was stored.

theme.jswhole filejavascript
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // "dark"
const lang = localStorage.getItem("lang");   // null: never stored

02Store objects as JSON

Turn the object into JSON text before saving and parse it when loading. Use ?? default for the missing case.

settings.jswhole filejavascript
const settings = { theme: "dark", fontSize: 18 };
localStorage.setItem("settings", JSON.stringify(settings));

const saved = JSON.parse(localStorage.getItem("settings") ?? "{}");
console.log(saved.fontSize + 2); // 20, a real number again

03Load safely with defaults

A loader that never throws and always returns a complete object is worth writing once and reusing.

load.jswhole filejavascript
const DEFAULTS = { theme: "light", fontSize: 16 };
function loadSettings() {
  try {
    const raw = localStorage.getItem("settings");
    return { ...DEFAULTS, ...(raw ? JSON.parse(raw) : {}) };
  } catch {
    return { ...DEFAULTS }; // broken JSON or storage blocked
  }
}

04Pick the right place for the data

Each storage has its own lifetime and reach. Choose by asking "how long should this live, and who should see it?"

Pick the right place for the datadiagram
Rendering diagram…

05Sync between tabs

The storage event fires in the other tabs of the same origin, not in the tab that made the change. It doesn't fire for sessionStorage changes in other tabs, because those tabs have their own copy. (The Hectal preview is a single sandboxed page, so you can't see this event here; try it on a real site with two tabs.)

sync.jswhole filejavascript
window.addEventListener("storage", (e) => {
  if (e.key === "theme") applyTheme(e.newValue);
  if (e.key === "token" && e.newValue === null) showLoggedOut();
});

06Look inside with DevTools

DevTools → Application (Chrome, Edge) or Storage (Firefox, Safari) → Local Storage → your origin. You can read, edit and delete entries there, which is exactly why your code must cope with values it didn't write.

Try it yourself

  1. 1

    Fix the boolean

    In the first example, predict what localStorage.getItem("isAdmin") === "false" prints, then add it. Then store isAdmin with JSON.stringify(false) and read it with JSON.parse: what does the ternary print now?

  2. 2

    Add a setting

    In the theme example, add compact: false to DEFAULTS and log loadSettings() again after saving. Predict: does the new key appear even though the stored JSON doesn't have it yet? Why?

  3. 3

    Watch the storage event for real

    Open any site in two tabs. In tab A's console run addEventListener("storage", (e) => console.log(e.key, e.newValue)). In tab B's console run localStorage.setItem("hello", "there"). Tab A prints hello there; tab B prints nothing.

Code & diagrams

Everything becomes a string New tab

`false` was stored as the string `"false"`, which is truthy. The object became `"[object Object]"`. JSON fixes both. Real browsers may list keys in a different order; never rely on it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

string 51
truthy!
[object Object]
null
6
Asha
keys: 3
  count
  isAdmin
  user
A theme switch that remembers (safe loader with defaults) ES2018+ New tab

The broken saved value didn't crash the page: the loader fell back to defaults. Tick the box: the page turns dark and the stored JSON updates. On a real site it would still be dark after a reload.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

bad saved settings, using defaults: SyntaxError
{ dark: false, size: 16 }
{"dark":false,"size":16}
sessionStorage: keep a form draft for this tab ES2020+ New tab

The code fakes one keystroke and one click so the console has something to show. Type in the box yourself afterwards and watch the status line.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

draft: Hello Asha
after send: null

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Parse a value that was never JSON

Store localStorage.setItem("theme", "dark") and later read it with JSON.parse(localStorage.getItem("theme")).

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Unexpected token 'd', "dark" is not valid JSON

Break #2

Fill up the quota

On a real site, run localStorage.setItem("big", "x".repeat(6 * 1024 * 1024)) (Hectal's in-memory preview has no limit, so do this in a normal tab).

terminal
$ Console
── what you'll see ──
Uncaught QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'big' exceeded the quota.

Myth vs fact

Myth

localStorage is a safe place for a login token because only my site can read it.

Fact

Only your origin can read it, but that includes every script running on your page: your code, third-party widgets, and any script an attacker injects through XSS. HttpOnly cookies are out of JavaScript's reach.

Myth

sessionStorage is shared between tabs of the same site.

Fact

Each tab has its own. Opening the same page in a new tab starts with empty sessionStorage (only duplicating a tab copies it).

Myth

localStorage keeps data forever.

Fact

Users clear it, private windows discard it, browsers may evict it when the disk is full, and Safari removes script-written storage for sites not visited for about seven days. Treat it as a cache.

Myth

localStorage.theme = "dark" is the same as setItem.

Fact

It often works, but it collides with method names (localStorage.key = 1 breaks key()), and some environments, including Hectal's preview, don't support it. Use setItem/getItem.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Storage is partitioned: in modern browsers a third-party iframe's storage is keyed by the top-level site too, so an embed on a.com and the same embed on b.com see different data. This blocks cross-site tracking through storage.

  • ▸

    Because Web Storage is synchronous and on the main thread, Chrome loads an origin's whole localStorage into memory on first access. Reading it during start-up is cheap after that, but large values still cost memory and serialisation time. Batch writes (for example, debounce saves) instead of writing on every keystroke.

  • ▸

    navigator.storage.estimate() reports usage and quota for all storage (IndexedDB, Cache API and so on), and navigator.storage.persist() asks the browser not to evict your data under storage pressure. Web Storage isn't available in Web Workers or Service Workers; IndexedDB is.

  • ▸

    Server-rendered apps (Next.js and similar) can't read localStorage on the server. Reading it during the first render causes hydration mismatches; read it in an effect or a store after mount, and use a small inline script in the <head> for things like theme to avoid a flash.

Remember this

  1. 1

    Web Storage is two objects the browser gives every page: localStorage and sessionStorage. Both work the same way: you store a value under a key (a name), like a labelled box. The methods are setItem(key, value), getItem(key) (returns null if the key doesn't exist), removeItem(key), clear() (removes everything), plus length and key(index) to list what's there. Storage belongs to the origin (scheme + host + port, Topic 11.1): https://hectal.in can't see what https://example.com stored.

  2. 2

    **localStorage lasts until the code or the user clears it: across reloads, closed tabs and browser restarts. It's shared by every tab of the same origin. sessionStorage belongs to one tab**: it survives reloads in that tab, but another tab of the same site gets its own empty copy, and it's deleted when the tab closes. (Duplicating a tab copies it.) Use localStorage for preferences such as theme or language; use sessionStorage for things that belong to one visit, such as a half-filled form or the step of a wizard.

  3. 3

    Web Storage stores only strings. Any value is converted with String(): 5 becomes "5", true becomes "true", and an object becomes the useless text "[object Object]". So structured data goes through JSON: setItem("settings", JSON.stringify(settings)) to save, JSON.parse(getItem("settings")) to load. Numbers need converting back too: Number(getItem("count")). Forgetting this gives bugs like "5" + 1 === "51".

  4. 4

    Treat stored data as untrusted and possibly missing. The user can edit it in DevTools, an older version of your app may have saved a different shape, and JSON.parse throws on broken text. A safe loader reads the key, returns a default when it's null, wraps JSON.parse in try/catch, and merges with defaults so new settings get sensible values. Writing can fail too: when the quota is full, setItem throws a QuotaExceededError, and some privacy settings block storage entirely (accessing it throws a SecurityError).

  5. 5

    The limits: about 5 MB per origin in current browsers (counted in characters, so roughly 5 million characters), and the API is synchronous: every call blocks the main thread while the browser reads or writes, so storing large data on every keystroke can make typing laggy. For big or structured data (many records, files, offline apps) use IndexedDB, which is asynchronous and can hold far more. When another tab changes localStorage, every other tab of that origin gets a storage event with key, oldValue and newValue, handy for syncing a logout or theme across tabs.

  6. 6

    Never store secrets (passwords, long-lived access tokens, personal data) in Web Storage. Any JavaScript running on your page can read it, so a single XSS bug (cross-site scripting: an attacker getting their script to run on your page, Topic 8.3) leaks everything stored there. Login sessions are safer in cookies marked HttpOnly (JavaScript can't read them), Secure and SameSite. Also remember that Safari may delete script-written storage for sites the user hasn't visited in about a week, and private windows throw it away at the end of the session: storage is a cache of convenience, not a database.

Explain it without notes

01

Compare localStorage, sessionStorage and cookies: lifetime, scope, size and who can read them.

02

Why do you need JSON.stringify and JSON.parse with Web Storage, and what goes wrong without them?

03

Why shouldn't you store an authentication token in localStorage?

04

How does the storage event work?

Practice

01

Write saveList(key, array) and loadList(key) that store an array as JSON. loadList must return [] when nothing is stored or the stored text is broken.

02

Make a visit counter: read visits from localStorage, add 1, save it, and print Visit number N. (In the preview it always prints 1, because storage resets on each Run.)

Trade-offs

  • ↔

    Web Storage is tiny and simple but synchronous and string-only; IndexedDB handles large structured data asynchronously but has a clunky API (small wrappers such as idb-keyval help).

  • ↔

    Saving on every change keeps nothing lost but costs main-thread time; debouncing saves is faster but can lose the last second of edits if the tab is killed, so also save on pagehide.

Done when you can

  • I can save, read, remove and list keys with setItem, getItem, removeItem, length and key().

  • I store objects and numbers as JSON and parse them back.

  • I write loaders that survive missing or broken values.

  • I can choose between localStorage, sessionStorage, cookies and IndexedDB.

  • I never put secrets in Web Storage, and I can explain why.