Ad – 728Γ—90
πŸ”§ Intermediate JS

JavaScript Local Storage – Persisting Data in the Browser

Browser storage APIs let you persist data across page reloads and sessions. localStorage retains data indefinitely; sessionStorage clears when the tab closes; cookies travel with every HTTP request. Knowing when and how to use each one β€” safely β€” is an important skill for building real-world web applications.

⏱️ 20 min read 🎯 Intermediate πŸ“… Updated 2026

localStorage API

localStorage is a synchronous key-value store scoped to the current origin (protocol + domain + port). Data persists until explicitly cleared.

JavaScript
// setItem(key, value) – both key and value must be strings
localStorage.setItem('username', 'Alice');
localStorage.setItem('theme', 'dark');

// getItem(key) – returns string or null (not undefined)
const name = localStorage.getItem('username'); // 'Alice'
const missing = localStorage.getItem('nonexistent'); // null

// removeItem(key) – delete one entry
localStorage.removeItem('username');

// clear() – delete ALL entries for this origin
localStorage.clear();

// length – number of stored keys
console.log(localStorage.length); // 0 (after clear)

// Iterating all entries
localStorage.setItem('a', '1');
localStorage.setItem('b', '2');
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(key, value);
}

// Property-style access (works but not recommended – can shadow API methods)
localStorage.theme = 'light';      // sets 'theme' key
console.log(localStorage.theme);   // 'light'
β–Ά Output
// After setItem / getItem:
Alice        // getItem('username')
null         // getItem('nonexistent')
// After iterating:
a  1
b  2

Storing Objects with JSON

localStorage only stores strings. To store objects or arrays, serialise with JSON.stringify on write and deserialise with JSON.parse on read.

JavaScript
// Storing an object
const user = { name: 'Alice', role: 'admin', lastLogin: new Date().toISOString() };
localStorage.setItem('user', JSON.stringify(user));

// Reading it back
const stored = localStorage.getItem('user');
const parsedUser = stored ? JSON.parse(stored) : null;
console.log(parsedUser?.name); // 'Alice'

// Helper wrapper for safer access
const storage = {
  get(key, fallback = null) {
    try {
      const item = localStorage.getItem(key);
      return item !== null ? JSON.parse(item) : fallback;
    } catch {
      return fallback;
    }
  },
  set(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (e) {
      console.warn('localStorage.setItem failed:', e.message);
      return false;
    }
  },
  remove(key) {
    localStorage.removeItem(key);
  }
};

// Usage
storage.set('cartItems', [{ id: 1, qty: 2 }, { id: 3, qty: 1 }]);
const cart = storage.get('cartItems', []);
console.log(cart.length); // 2
πŸ’‘
Always Wrap localStorage in try/catch

localStorage can throw in private/incognito mode (quota exceeded or access denied), when the storage quota is full, or in certain browser extensions. Always wrap setItem in try/catch to handle these cases gracefully instead of crashing the app.

sessionStorage

sessionStorage has the same API as localStorage but data is cleared when the browser tab is closed. Separate tabs get separate sessionStorage instances even for the same origin.

JavaScript
// Same API as localStorage
sessionStorage.setItem('formDraft', JSON.stringify({ name: 'Alice', age: '' }));
const draft = JSON.parse(sessionStorage.getItem('formDraft') ?? '{}');

// Use case: save form state while user navigates away and back
function saveFormState(formId) {
  const form = document.getElementById(formId);
  const data = {};
  for (const el of form.elements) {
    if (el.name) data[el.name] = el.value;
  }
  sessionStorage.setItem(`form_${formId}`, JSON.stringify(data));
}

function restoreFormState(formId) {
  const saved = sessionStorage.getItem(`form_${formId}`);
  if (!saved) return;
  const data = JSON.parse(saved);
  const form = document.getElementById(formId);
  for (const [name, value] of Object.entries(data)) {
    const el = form.elements[name];
    if (el) el.value = value;
  }
}

// Tab-specific wizard state
sessionStorage.setItem('wizardStep', '3');
const step = parseInt(sessionStorage.getItem('wizardStep') ?? '1', 10);

Storage Events (Cross-Tab Communication)

JavaScript
// The 'storage' event fires in OTHER tabs (not the one that made the change)
window.addEventListener('storage', (event) => {
  console.log('Key changed:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('Storage area:', event.storageArea); // localStorage or sessionStorage
  console.log('URL of tab that changed:', event.url);

  // React to theme changes from another tab
  if (event.key === 'theme') {
    applyTheme(event.newValue);
  }

  // React to logout in another tab
  if (event.key === 'user' && event.newValue === null) {
    window.location.href = '/login';
  }
});

// Broadcast a message to all same-origin tabs
function broadcastLogout() {
  localStorage.setItem('logout', Date.now().toString());
  localStorage.removeItem('logout'); // clean up after firing event
}

// Better cross-tab communication: BroadcastChannel API
const channel = new BroadcastChannel('app_updates');
channel.postMessage({ type: 'THEME_CHANGED', theme: 'dark' });
channel.onmessage = (event) => {
  console.log('Message from another tab:', event.data);
};

localStorage vs sessionStorage vs Cookies

FeaturelocalStoragesessionStorageCookies
Capacity~5–10 MB~5–10 MB~4 KB
PersistenceUntil clearedUntil tab closesUntil expiry date
ScopeOrigin-widePer tabDomain (+ path)
Sent to serverNoNoYes (every request)
Accessible from JSYesYesYes (unless HttpOnly)
Cross-tab syncYes (via storage event)NoYes
Best forUser prefs, cacheForm state, wizard stepsAuthentication tokens (HttpOnly)

Practical Examples

JavaScript
// 1. Theme preference
function initTheme() {
  const saved = localStorage.getItem('theme') ?? 'light';
  document.documentElement.dataset.theme = saved;
  return saved;
}
function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.documentElement.dataset.theme = theme;
}

// 2. Shopping cart persistence
class Cart {
  constructor() {
    this.items = storage.get('cart', []);
  }
  add(product) {
    const existing = this.items.find(i => i.id === product.id);
    if (existing) existing.qty++;
    else this.items.push({ ...product, qty: 1 });
    this.save();
  }
  remove(productId) {
    this.items = this.items.filter(i => i.id !== productId);
    this.save();
  }
  get total() {
    return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  }
  save() {
    storage.set('cart', this.items);
  }
  clear() {
    this.items = [];
    localStorage.removeItem('cart');
  }
}

// 3. Recently visited pages
function trackVisit(page) {
  const recent = storage.get('recentPages', []);
  const updated = [page, ...recent.filter(p => p !== page)].slice(0, 10);
  storage.set('recentPages', updated);
}
trackVisit('/javascript/intermediate/local-storage.html');

IndexedDB – Brief Introduction

JavaScript
// IndexedDB is an asynchronous, structured database in the browser
// Use it when localStorage's ~5MB or string-only limitation is too restrictive

// Opening a database
const request = indexedDB.open('myDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // Create an object store (like a table)
  const store = db.createObjectStore('users', { keyPath: 'id' });
  store.createIndex('name', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
  // Add a record
  const tx = db.transaction('users', 'readwrite');
  tx.objectStore('users').add({ id: 1, name: 'Alice', age: 28 });
};

// For real projects, use the idb library (a Promise-based wrapper)
// import { openDB } from 'idb';
// const db = await openDB('myDB', 1, { upgrade(db) { db.createObjectStore('kv'); }});
// await db.put('kv', 'hello', 'greeting');
// const value = await db.get('kv', 'greeting'); // 'hello'
πŸ’‘
Never Store Sensitive Data in localStorage

Any JavaScript on the page can read localStorage. Never store authentication tokens, passwords, payment information, or any sensitive personal data in localStorage or sessionStorage. Use HttpOnly cookies for auth tokens (they cannot be read by JavaScript) and keep sensitive data server-side.

Interview Questions

  • What is the difference between localStorage and sessionStorage?
  • Why does the storage event not fire in the tab that made the change?
  • How do you store an object in localStorage?
  • What are the security risks of using localStorage for authentication?
  • When would you use IndexedDB instead of localStorage?
  • What happens when the localStorage quota is exceeded?

πŸ‹οΈ Practical Exercise

Build a useLocalStorage(key, initialValue) function (not a React hook β€” a plain JS utility) that:

  1. Returns the current stored value (or initialValue if not set).
  2. Accepts a setter function that updates both the stored value and localStorage.
  3. Handles JSON serialisation/deserialisation automatically.
  4. Listens to the storage event so the returned value stays in sync across tabs.

πŸ”₯ Challenge Exercise

Create a StorageManager class that abstracts over localStorage with these features: automatic expiry (store data with a TTL and return null when expired), namespacing (all keys prefixed to avoid collisions), a subscribe(key, callback) method for reactive updates via the storage event, and automatic compression detection using localStorage.length to warn when nearing the 5MB limit.

Frequently Asked Questions

Is localStorage synchronous and does that matter?
Yes, it is synchronous. For small amounts of data this is fine. For large reads/writes or high-frequency operations (e.g., saving on every keystroke in a complex app), the synchronous I/O can block the main thread. In those cases, debounce saves or switch to IndexedDB which is fully asynchronous.
Can localStorage be accessed from a Service Worker?
No. Service Workers run in a separate thread and do not have access to localStorage or sessionStorage. For Service Worker storage, use the Cache API or IndexedDB.
Why do cookies have a much smaller size limit than localStorage?
Cookies are sent with every HTTP request to the matching domain. Keeping them small (~4KB) minimises the HTTP overhead on every request. localStorage and sessionStorage exist purely client-side and are never sent automatically to the server, so they can afford a much larger limit (~5–10MB).
Is data in localStorage encrypted?
No. Data is stored as plain text on the user's disk. Any code running in the same origin can read it, and an attacker with physical access to the device can read it too. For sensitive data, always encrypt before storing, use HttpOnly cookies, or keep the data on the server.
What is the difference between localStorage.getItem('key') and localStorage.key?
Both return the stored value for most keys. However, property-style access can accidentally shadow the localStorage API if a key has the same name as an API method (e.g., localStorage.clear would give you the stored value, not the function). Always use getItem/setItem for safety.
Ad – 336Γ—280

πŸ“‹ Summary

  • localStorage stores data indefinitely; sessionStorage clears when the tab closes.
  • Both use the same API: setItem, getItem, removeItem, clear, length, key(i).
  • All stored values are strings β€” use JSON.stringify/JSON.parse for objects.
  • Always wrap storage calls in try/catch to handle quota exceeded and private mode errors.
  • The storage event fires in other tabs when localStorage changes β€” use it for sync.
  • Use cookies (HttpOnly) for authentication tokens; localStorage for UI preferences and caches.
  • Never store passwords, tokens, or sensitive personal data in localStorage.
  • Use IndexedDB (or the idb library) when you need more than ~5MB or structured querying.