Skip to main content

πŸ’Ύ Local Storage and Session Storage

Your browser ships with a tiny key-value database that survives page reloads β€” no server, no cookies, no libraries. In this lesson you'll learn to store and read data with the Web Storage API, decide when to reach for localStorage versus sessionStorage, and dodge the sharp edges: JSON serialization, quota limits, and the very real security risks.

🎯 Learning Objectives

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

  • Explain the difference between localStorage and sessionStorage and choose the right one for a task
  • Use setItem, getItem, removeItem, and clear to manage stored data
  • Serialize objects and arrays with JSON.stringify/JSON.parse and add expiry logic
  • Synchronize state across browser tabs using the storage event
  • Handle quota errors and identify what must never be stored client-side

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

Hands-on: Build a form that auto-saves as you type and restores itself after a reload.

In This Lesson

What Is Web Storage?

The Web Storage API lets a website keep small amounts of data directly inside the user's browser, tied to your site's origin. Before it existed, developers stuffed everything into cookies β€” which are limited to about 4 KB, are sent to the server on every single request (wasting bandwidth), and require awkward string parsing.

Web Storage fixes all three problems: roughly 5 MB of room per origin, data that stays in the browser and is never automatically transmitted, and a dead-simple key/value interface.

πŸ’‘ A useful analogy: localStorage is a filing cabinet in your office β€” it's still there tomorrow when you come back. sessionStorage is a whiteboard in a meeting room β€” perfect for working notes, but wiped clean the moment you leave.

πŸ“– Key Terms

Origin: the combination of scheme + host + port (e.g. https://shop.example.com). Storage is isolated per origin β€” a subdomain is a different origin.

Key/value store: data is saved as named strings; you look items up by their key, like words in a dictionary.

Serialization: converting a structured value (object, array) into a string so it can be stored, then back again.

localStorage vs sessionStorage

Both objects expose the exact same API. The only difference is lifetime and scope:

Comparison of localStorage and sessionStorage localStorage persists indefinitely and is shared across all tabs of an origin; sessionStorage lives only for one tab's session and is cleared when the tab closes. localStorage Persists until deleted Survives browser restart Shared across all tabs ~5 MB per origin Preferences Β· carts Β· caches sessionStorage Cleared when tab closes Survives page reloads Isolated to one tab ~5 MB per origin Wizards Β· scratch state
Figure 1 β€” Same API, different lifetimes. Pick localStorage when data should outlive the tab, sessionStorage when it should not.
flowchart TD A[Web Storage] --> B[localStorage] A --> C[sessionStorage] B --> D[Persists indefinitely] B --> E[Shared across tabs & windows] C --> F[Cleared on tab close] C --> G[Isolated to one tab]

How a "session" behaves

  • Navigating within the same tab keeps sessionStorage.
  • Reloading or restoring the page keeps it.
  • Opening the same site in a new tab starts a fresh, empty sessionStorage.
  • Duplicating a tab copies the current sessionStorage into the new one.
  • Closing the tab destroys it.
FeatureCookiesWeb Storage
Capacity~4 KB~5 MB
Sent with every request?Yes (adds latency)No (stays in browser)
APIManual string parsingSimple key/value methods
ExpirationConfigurableNone or session-based

The Core API

Five methods and one property cover everything. Every value is stored and returned as a string β€” remember that, because it trips up almost everyone at least once.

// Store values (both arguments are coerced to strings)
localStorage.setItem('username', 'ada_lovelace');
localStorage.setItem('visits', '1');

// Read a value back β€” returns null if the key is missing
const name = localStorage.getItem('username'); // 'ada_lovelace'
const missing = localStorage.getItem('nope');  // null

// Remove one key, or wipe everything for this origin
localStorage.removeItem('visits');
// localStorage.clear();

// Inspect the store
console.log(localStorage.length);   // number of keys
console.log(localStorage.key(0));   // the key at index 0

⚠️ Everything is a string

Numbers, booleans, and objects are all coerced to text. localStorage.setItem('n', 5) stores the string "5", and getItem('n') returns "5", not 5. Convert on the way out: Number(localStorage.getItem('n')).

You can also use property syntax (localStorage.username = 'ada'), but prefer the methods β€” they read more clearly and avoid clashing with built-in property names like length or clear.

Storing Objects & Arrays

Because storage only holds strings, structured data must be run through JSON on the way in and out.

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

// Serialize to a string before storing
localStorage.setItem('prefs', JSON.stringify(prefs));

// Parse back into an object when reading
const saved = JSON.parse(localStorage.getItem('prefs'));
console.log(saved.notifications.email); // true

A robust read guards against a missing or corrupted value so one bad entry can't crash your app:

function readJSON(key, fallback = null) {
  const raw = localStorage.getItem(key);
  if (raw === null) return fallback;
  try {
    return JSON.parse(raw);
  } catch {
    console.warn(`Corrupt JSON in "${key}" β€” using fallback`);
    return fallback;
  }
}

const searches = readJSON('recentSearches', []); // always an array

⚠️ JSON loses some types

Date objects become ISO strings, undefined and functions are dropped, and NaN/Infinity become null. If you need dates back as real Date objects, re-hydrate them yourself: new Date(saved.createdAt).

Cross-Tab Sync & Expiry

The storage event

When localStorage changes in one tab, every other tab on the same origin fires a storage event. (The tab that made the change does not receive it.) This gives you free cross-tab synchronization β€” change a setting in one tab and watch the others update live.

window.addEventListener('storage', (event) => {
  if (event.key === 'prefs') {
    console.log('Prefs changed in another tab');
    console.log('old:', event.oldValue, 'new:', event.newValue);
    applyPreferences(JSON.parse(event.newValue));
  }
});

Adding your own expiry

Unlike cookies, Web Storage has no built-in expiration. You can bolt it on by storing a timestamp alongside the value:

function setWithExpiry(key, value, ttlMs) {
  const record = { value, expiresAt: Date.now() + ttlMs };
  localStorage.setItem(key, JSON.stringify(record));
}

function getWithExpiry(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  const { value, expiresAt } = JSON.parse(raw);
  if (Date.now() > expiresAt) {
    localStorage.removeItem(key); // lazily evict on read
    return null;
  }
  return value;
}

// Cache a result for 10 minutes
setWithExpiry('rates', { usd: 1.0 }, 10 * 60 * 1000);

πŸ’‘ Modern alternative: BroadcastChannel

For deliberate tab-to-tab messaging (not just storage changes), the BroadcastChannel API is cleaner: const ch = new BroadcastChannel('app'); ch.postMessage(data);. Reach for the storage event when the source of truth is the stored data.

Limits & Security

Quota handling

When you exceed the ~5 MB limit, setItem throws a QuotaExceededError. Never let that crash a save β€” wrap it:

function safeSet(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (err) {
    if (err.name === 'QuotaExceededError') {
      console.warn('Storage full β€” evicting old data');
      evictOldEntries();
      return false;
    }
    throw err; // some other, unexpected error
  }
}

πŸ”’ Never store secrets client-side

Web Storage is plaintext and readable by any JavaScript running on your page β€” including code injected by a cross-site scripting (XSS) attack. Do not put these here:

  • Authentication tokens or session IDs (an XSS bug becomes full account takeover)
  • Passwords or API keys
  • Personal identifiable information (PII) or payment details

For auth tokens, prefer an HttpOnly, Secure, SameSite cookie the browser sends automatically but JavaScript cannot read.

Also note: some browsers block or wipe storage in private/incognito mode, and Safari may throw the moment you call setItem. Feature-detect before relying on it:

function storageAvailable() {
  try {
    const t = '__test__';
    localStorage.setItem(t, t);
    localStorage.removeItem(t);
    return true;
  } catch {
    return false; // disabled, full, or private mode
  }
}

Hands-on: Auto-Saving Form

πŸ‹οΈ Build a form that never loses your work

Objective: Save form input to localStorage as the user types, restore it on reload, and clear it on successful submit.

Starter HTML

<form id="application">
  <input name="fullName" placeholder="Full name">
  <input name="email" type="email" placeholder="Email">
  <textarea name="coverLetter" placeholder="Cover letter"></textarea>
  <button type="submit">Submit</button>
</form>
<p id="status" aria-live="polite"></p>

Your tasks

  1. On every input event, collect all field values into an object and save it under one key.
  2. On page load, read that key and repopulate the fields.
  3. On submit, remove the saved key so a fresh visit starts blank.
  4. Bonus: debounce the save so it runs at most once every 400 ms.
πŸ’‘ Hint

Gather values with new FormData(form) then Object.fromEntries(...). To repopulate, loop the saved object and set form.elements[name].value. A debounce is just a setTimeout you clearTimeout on each keystroke.

βœ… Solution
const form = document.getElementById('application');
const status = document.getElementById('status');
const KEY = 'applicationDraft';

// 1. Restore on load
const draft = JSON.parse(localStorage.getItem(KEY) || '{}');
for (const [name, value] of Object.entries(draft)) {
  if (form.elements[name]) form.elements[name].value = value;
}

// 2. Save on input (debounced)
let timer;
form.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(() => {
    const data = Object.fromEntries(new FormData(form));
    localStorage.setItem(KEY, JSON.stringify(data));
    status.textContent = 'Draft saved ' + new Date().toLocaleTimeString();
  }, 400);
});

// 3. Clear on submit
form.addEventListener('submit', (e) => {
  e.preventDefault();
  localStorage.removeItem(KEY);
  status.textContent = 'Submitted β€” draft cleared';
  form.reset();
});

Open the page, type a few fields, reload β€” your text is still there. Submit, reload β€” it's gone. That's the whole feature in ~25 lines.

Best Practices

βœ… Do

  • Wrap every JSON.parse in a try/catch and provide a fallback value.
  • Namespace your keys (myapp:prefs) so different features don't collide.
  • Feature-detect availability before depending on storage.
  • Use sessionStorage for anything that shouldn't leak between visits.

🚫 Don't

  • Store tokens, passwords, or PII β€” treat storage as public to your own scripts.
  • Assume values are anything but strings.
  • Use it as a real database β€” for large or queryable data, use IndexedDB.
  • Forget that quota exists; guard setItem against QuotaExceededError.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • localStorage persists forever and is shared across tabs; sessionStorage lives only for one tab's session.
  • The API is five methods β€” setItem, getItem, removeItem, clear, key β€” and everything is stored as a string.
  • Use JSON.stringify/JSON.parse for objects, and defend parsing with try/catch.
  • The storage event syncs data across tabs for free.
  • It's plaintext and XSS-readable β€” never store secrets, and guard against quota errors.

🎯 Quick Quiz

Question 1: A user fills out a multi-step wizard and you want the progress gone the moment they close the tab. Which store fits best?

Question 2: You run localStorage.setItem('count', 5) then localStorage.getItem('count'). What comes back?

Question 3: Why is storing a login token in localStorage risky?

πŸ“š Further Reading

πŸš€ What's Next?

Next we leave the filing cabinet behind and ask the browser a very different question: where are you? The Geolocation API lets your app read the user's coordinates β€” with their permission β€” to power maps, store finders, and weather.

πŸŽ‰ Nice work!

You can now persist state entirely in the browser. Your forms will never lose a user's work again.