Skip to main content

πŸ’Ύ LocalStorage and SessionStorage

The browser can remember things for you β€” a chosen theme, an in-progress form, a shopping cart β€” without ever touching a server. The Web Storage API gives you a dead-simple key/value store that lives right on the user's device. In this lesson you'll learn when to reach for localStorage versus sessionStorage, how to store real objects safely, and the security lines you must never cross.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain how Web Storage differs from cookies and choose the right tool for a job
  • Use setItem, getItem, removeItem, and clear to manage stored data
  • Persist and rehydrate objects and arrays with JSON.stringify / JSON.parse, including dates
  • Contrast localStorage (persistent) with sessionStorage (per-tab, temporary)
  • Synchronise state across tabs with the storage event and handle QuotaExceededError gracefully
  • Identify what should never be stored client-side and why

Estimated Time: 35–45 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Build a persistent "remember my preferences" panel backed by localStorage.

In This Lesson

What Is Web Storage?

The Web Storage API lets a website save data directly in the visitor's browser as simple key/value pairs. Both keys and values are always strings. It comes in two flavours that share one identical API but differ in how long the data lives:

  • localStorage β€” persists indefinitely. It survives page reloads, tab closes, and even a full browser restart. It only goes away when your code deletes it or the user clears their browser data.
  • sessionStorage β€” lasts only for the lifetime of a single tab. Close the tab and it's gone.
πŸ’‘ A useful analogy: localStorage is the notebook you keep on your desk β€” write in it today, and it's still there next week. sessionStorage is a sticky note you scribble during one work session and toss when you get up. Cookies, by contrast, are tiny slips you carry back and forth to the server in your pocket every single trip.

Web Storage sits alongside several other client-side storage mechanisms. Knowing where each fits keeps you from reaching for the wrong one:

flowchart TD A[Browser storage] --> B[Web Storage API] A --> C[Cookies] A --> D[IndexedDB] A --> E[Cache API] B --> F[localStorage] B --> G[sessionStorage] C -.->|sent to server| S[(Server)] D -->|large structured data| H[Object stores] E -->|offline assets| I[Service Worker]

πŸ“– Key Terms

Origin: the combination of scheme + host + port (e.g. https://shop.example.com:443). Storage is scoped to one origin and never shared with another.

Serialization: turning an in-memory value (like an object) into a string that can be stored, and back again.

Quota: the storage budget a browser grants an origin β€” commonly around 5 MB for Web Storage.

Web Storage vs. Cookies

Before Web Storage existed, cookies were the only client-side store. They still have a job β€” but for most "remember this in the browser" tasks, Web Storage is simpler and faster. Here's the comparison at a glance:

Feature Cookies localStorage sessionStorage
Capacity ~4 KB per cookie ~5 MB per origin ~5 MB per origin
Lifetime Explicit expiry date Until deleted Until the tab closes
Sent with every request? Yes β€” added to HTTP headers automatically No β€” client-side only No β€” client-side only
Readable by the server? Yes No No
API String parsing of document.cookie Clean key/value methods Clean key/value methods

βœ… Rule of thumb

Use a cookie when the server needs the value on every request (session IDs, auth tokens set as HttpOnly). Use Web Storage when only your front-end JavaScript needs it (UI preferences, drafts, caches). Sending 5 MB of preferences to the server on every page load would be a disaster β€” that's exactly why Web Storage exists.

localStorage Fundamentals

The whole API is four methods plus a length property. Both keys and values must be strings β€” anything else is coerced to a string automatically, which is a common source of bugs.

// Store values (keys and values are strings)
localStorage.setItem('username', 'ray');
localStorage.setItem('darkMode', 'true');   // note: the string "true", not a boolean

// Read a value β€” returns null if the key does not exist
const name = localStorage.getItem('username');   // 'ray'
const missing = localStorage.getItem('nope');    // null

// A common gotcha: everything comes back as a string
const dark = localStorage.getItem('darkMode');
console.log(dark === true);   // false! it's the STRING 'true'
console.log(dark === 'true'); // true

// Remove one key, or wipe the whole origin's storage
localStorage.removeItem('darkMode');
localStorage.clear();

// Inspect what is stored
console.log(localStorage.length);   // number of keys
console.log(localStorage.key(0));   // the name of the first key

⚠️ Booleans and numbers become strings

localStorage.setItem('count', 5) stores the string "5". When you read it back you must convert it: Number(localStorage.getItem('count')). For booleans, compare against 'true' or store JSON. Forgetting this is the number-one Web Storage bug.

Handling a full quota

If you exceed the ~5 MB budget (or storage is disabled, as in some private-browsing modes), setItem throws a QuotaExceededError. Always wrap writes that could be large:

function safeSet(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (err) {
    if (err.name === 'QuotaExceededError') {
      console.error('Storage is full β€” clearing old caches.');
      // e.g. evict cached items, then retry, or fall back to memory
    }
    return false;
  }
}

Storing Objects, Arrays & Dates

Because values must be strings, you can't store an object directly β€” setItem('user', {name:'Ray'}) would save the useless string "[object Object]". The fix is JSON serialization: stringify on the way in, parse on the way out.

const prefs = {
  theme: 'dark',
  fontSize: 16,
  notifications: { email: true, push: false }
};

// Save: object -> JSON string
localStorage.setItem('prefs', JSON.stringify(prefs));

// Load: JSON string -> object (guard against missing/corrupt data)
const raw = localStorage.getItem('prefs');
const restored = raw ? JSON.parse(raw) : null;
console.log(restored?.notifications.email); // true

⚠️ Dates do not survive a round-trip

JSON.stringify turns a Date into an ISO string, and JSON.parse does not turn it back. You must rehydrate dates yourself.

const activity = { lastLogin: new Date() };
localStorage.setItem('activity', JSON.stringify(activity));

const back = JSON.parse(localStorage.getItem('activity'));
console.log(typeof back.lastLogin);        // 'string' β€” not a Date!

// Rehydrate it
back.lastLogin = new Date(back.lastLogin);
console.log(back.lastLogin.getFullYear()); // now it's a real Date

For a cleaner, reusable approach, wrap the pattern in a small helper that adds JSON handling, optional expiry, and error safety. Modern codebases build exactly this kind of utility once and reuse it everywhere:

const store = {
  // Save any JSON-serialisable value, with an optional TTL in minutes
  set(key, value, ttlMinutes) {
    const record = { value };
    if (ttlMinutes) record.expiresAt = Date.now() + ttlMinutes * 60_000;
    try {
      localStorage.setItem(key, JSON.stringify(record));
    } catch (err) {
      console.error('store.set failed:', err);
    }
  },

  // Read a value, honouring expiry; returns null if missing or stale
  get(key) {
    const raw = localStorage.getItem(key);
    if (!raw) return null;
    try {
      const record = JSON.parse(raw);
      if (record.expiresAt && Date.now() > record.expiresAt) {
        localStorage.removeItem(key);
        return null;
      }
      return record.value;
    } catch {
      return null; // corrupt JSON β€” treat as missing
    }
  },

  remove(key) { localStorage.removeItem(key); }
};

store.set('cart', [{ id: 1, qty: 2 }]);   // survives restarts
store.set('token', 'abc', 60);            // auto-expires in 60 minutes
console.log(store.get('cart'));           // [{ id: 1, qty: 2 }]

sessionStorage & Tab Isolation

sessionStorage has the exact same API as localStorage β€” swap the object name and everything works. The only difference is lifetime and scope: its data belongs to one browsing-context (tab) and vanishes when that tab closes.

// Perfect for one-visit, throwaway state
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('scrollY', String(window.scrollY));

const step = sessionStorage.getItem('checkoutStep'); // '2'
sessionStorage.clear(); // wiped automatically when the tab closes anyway

This tab-level isolation is a feature, not a limitation. Each tab gets its own private sessionStorage, so two tabs on the same site can run independent multi-step flows without stepping on each other. Meanwhile localStorage is shared across every tab of the origin:

sessionStorage is per-tab; localStorage is shared Two browser tabs on the same site each have their own separate sessionStorage box, while a single shared localStorage box is connected to both tabs. Tab 1 Β· shop.com private sessionStorage Tab 2 Β· shop.com private sessionStorage session A session B shared localStorage one box for the whole origin
Figure 1 β€” Each tab keeps its own sessionStorage, but every tab of the same origin reads and writes the one shared localStorage.

πŸ’‘ Quirk: duplicating a tab

If a user duplicates a tab (right-click β†’ Duplicate), the new tab starts with a one-time copy of the original's sessionStorage. From that moment the two are independent. Opening a fresh tab to the same URL, however, always starts empty.

Syncing Tabs with the storage Event

Here's a genuinely useful trick. When localStorage changes in one tab, the browser fires a storage event in every other tab of the same origin. That lets you keep multiple open tabs in sync β€” flip the theme in one, and the rest follow instantly.

// Runs in OTHER tabs when localStorage changes (never in the tab that wrote it)
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.classList.toggle('dark', event.newValue === 'dark');
  }
});

⚠️ Two things that surprise people

The event does not fire in the same tab that made the change β€” only in the others. And sessionStorage changes never fire it at all, because that data is private to a single tab. Use it for cross-tab localStorage sync only.

Security & Privacy

Web Storage is convenient, but it is not a vault. Data is stored unencrypted on disk and is readable by any JavaScript running on your origin. That has direct consequences for what belongs there.

⚠️ Never store these client-side

  • Passwords or password hashes
  • Long-lived authentication tokens (a stolen token = a stolen session). Prefer HttpOnly cookies, which JavaScript cannot read.
  • Credit-card numbers or other financial data
  • Personally identifying information (PII) you wouldn't want an XSS attacker to exfiltrate

βœ… Good candidates for Web Storage

  • UI preferences β€” theme, language, layout, "I've dismissed this banner"
  • Non-sensitive drafts and in-progress form data
  • Short-lived, non-critical caches of public API responses
  • Anonymous, non-identifying app state

The core threat is XSS (cross-site scripting): if an attacker can inject script into your page, they can read everything in Web Storage. That's why an HttpOnly auth cookie beats a token in localStorage β€” the cookie is invisible to script. Whatever you do store, clear the sensitive parts on logout:

function logout() {
  // Remove anything tied to the authenticated session
  localStorage.removeItem('authToken');
  localStorage.removeItem('userId');
  sessionStorage.clear();
  // Keep harmless preferences like theme if you like
  window.location.href = '/login.html';
}

Finally, always feature-detect before relying on storage β€” some enterprise or privacy configurations disable it, and a bare access can throw:

function storageAvailable(type) {
  try {
    const s = window[type];
    const t = '__test__';
    s.setItem(t, t);
    s.removeItem(t);
    return true;
  } catch {
    return false;
  }
}

if (storageAvailable('localStorage')) {
  enablePersistentFeatures();
} else {
  fallBackToInMemoryState();
}

Hands-on Exercise

πŸ‹οΈ Build a "Remember My Preferences" Panel

Objective: Persist a small set of UI preferences with localStorage so they survive a page reload.

Instructions:

  1. Create a page with a theme <select> (light/dark), a font-size <input type="range">, and a "Save" button.
  2. On load, read a prefs object from storage (via JSON.parse) and apply it to the controls and the <body>.
  3. On Save, gather the current control values into one object and write it with JSON.stringify.
  4. Reload the page and confirm your choices stick. Then open DevTools β†’ Application β†’ Local Storage and watch the value change.
  5. Stretch: add a storage event listener so opening the page in a second tab and saving there updates the first tab live.
πŸ’‘ Hint

Store a single object under one key rather than many separate keys β€” it's easier to load and clear as a unit. Remember that the range input's .value is a string; use Number() if you do maths with it, or just keep it as a string for CSS.

βœ… Sample solution
const KEY = 'prefs';
const themeSel = document.querySelector('#theme');
const sizeInp  = document.querySelector('#size');
const saveBtn  = document.querySelector('#save');

function apply(prefs) {
  document.body.dataset.theme = prefs.theme;
  document.body.style.fontSize = prefs.fontSize + 'px';
  themeSel.value = prefs.theme;
  sizeInp.value = prefs.fontSize;
}

function load() {
  const raw = localStorage.getItem(KEY);
  const prefs = raw ? JSON.parse(raw) : { theme: 'light', fontSize: 16 };
  apply(prefs);
}

saveBtn.addEventListener('click', () => {
  const prefs = { theme: themeSel.value, fontSize: Number(sizeInp.value) };
  localStorage.setItem(KEY, JSON.stringify(prefs));
  apply(prefs);
});

// Stretch: live cross-tab sync
window.addEventListener('storage', (e) => {
  if (e.key === KEY && e.newValue) apply(JSON.parse(e.newValue));
});

load();

🎯 Quick Quiz

Question 1: You run localStorage.setItem('count', 5) then localStorage.getItem('count') === 5. What is the result?

Question 2: Which statement about sessionStorage is true?

Question 3: Why is a long-lived auth token safer in an HttpOnly cookie than in localStorage?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Web Storage is a simple string key/value store scoped to one origin, with a ~5 MB budget.
  • localStorage persists until deleted; sessionStorage is per-tab and dies with the tab β€” same API, different lifetime.
  • Store objects with JSON.stringify / JSON.parse, and remember to rehydrate dates manually.
  • Everything is stored as a string β€” convert numbers and booleans on the way out.
  • The storage event syncs localStorage across tabs; it never fires for sessionStorage or in the writing tab.
  • Never keep passwords, financial data, or long-lived tokens client-side; feature-detect and clear on logout.

πŸ“š Further Reading

πŸš€ What's Next?

Web Storage is just one of dozens of capabilities the browser exposes to JavaScript. Next we'll zoom out and survey the whole landscape of Browser APIs β€” from geolocation and the camera to background workers β€” so you know what tools are on the shelf.

πŸŽ‰ Nice work!

Your apps can now remember things. Let's see everything else the browser can do.