Toolmingo
Guides6 min read

How to Use localStorage in JavaScript (With sessionStorage Comparison)

Learn how to use localStorage and sessionStorage in JavaScript. Covers setItem/getItem/removeItem, storing objects, storage events, size limits, security, and common mistakes.

localStorage lets your web app save data directly in the browser — no server, no database, no expiry. It's one of the most useful browser APIs, and also one of the most frequently misused.

This guide covers everything: the API, object storage, storage events, size limits, security, and the common mistakes you'll encounter in real projects.


Quick-reference table

Feature localStorage sessionStorage
Lifetime Until explicitly cleared Until tab is closed
Scope All tabs/windows on the same origin Current tab only
Capacity ~5–10 MB (per origin) ~5–10 MB (per tab)
Accessible from JS Yes Yes
Sent to server No No
Available in service worker No No
Survives page reload Yes Yes
Survives browser close Yes No

Basic operations

Both localStorage and sessionStorage share the same API. Every value must be a string.

// Set a value
localStorage.setItem('username', 'alice');

// Get a value (returns null if key doesn't exist)
const name = localStorage.getItem('username'); // 'alice'

// Remove a single key
localStorage.removeItem('username');

// Clear everything for this origin
localStorage.clear();

// Check how many keys are stored
console.log(localStorage.length); // 0

The same methods work for sessionStorage — just swap the name:

sessionStorage.setItem('draft', 'Hello world');
const draft = sessionStorage.getItem('draft');

Storing objects (JSON.stringify / JSON.parse)

localStorage only stores strings. To save objects or arrays, serialize them first:

const user = { id: 42, name: 'Alice', roles: ['admin', 'editor'] };

// Save
localStorage.setItem('user', JSON.stringify(user));

// Load
const stored = localStorage.getItem('user');
const parsed = stored ? JSON.parse(stored) : null;

console.log(parsed.roles); // ['admin', 'editor']

Helper functions to avoid repetition:

function lsSet(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

function lsGet(key, fallback = null) {
  const raw = localStorage.getItem(key);
  if (raw === null) return fallback;
  try {
    return JSON.parse(raw);
  } catch {
    return raw; // wasn't JSON — return as string
  }
}

function lsRemove(key) {
  localStorage.removeItem(key);
}

// Usage
lsSet('settings', { theme: 'dark', fontSize: 16 });
const settings = lsGet('settings', { theme: 'light', fontSize: 14 });

Iterating over all keys

// Option 1: index-based
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(key, value);
}

// Option 2: Object.entries (works because localStorage is array-like)
Object.entries(localStorage).forEach(([key, value]) => {
  console.log(key, value);
});

Listening for storage changes

The storage event fires on other tabs in the same origin when localStorage changes. Useful for syncing state between tabs.

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 object:', event.storageArea); // localStorage or sessionStorage
  console.log('Origin:', event.url);
});

Note: The storage event does not fire in the tab that made the change — only in other tabs/windows with the same origin. To react in the same tab, use a custom event or a state management pattern.


Checking available space

There's no standard API to check remaining quota, but you can estimate it:

function estimateLocalStorageUsage() {
  let total = 0;
  for (const key of Object.keys(localStorage)) {
    total += key.length + (localStorage.getItem(key) || '').length;
  }
  return `~${(total / 1024).toFixed(1)} KB used`;
}

When storage is full, setItem throws a QuotaExceededError:

try {
  localStorage.setItem('key', veryLargeString);
} catch (err) {
  if (err.name === 'QuotaExceededError') {
    console.warn('Storage full — consider clearing old data');
  }
}

Common patterns

Theme preference

// Save
localStorage.setItem('theme', 'dark');

// Apply on load (before paint to avoid flash)
const theme = localStorage.getItem('theme') ?? 'light';
document.documentElement.setAttribute('data-theme', theme);

Form autosave (draft)

const input = document.querySelector('#essay');

// Restore draft
input.value = localStorage.getItem('essay-draft') ?? '';

// Save on every change (debounced)
input.addEventListener('input', debounce(() => {
  localStorage.setItem('essay-draft', input.value);
}, 500));

// Clear on submit
document.querySelector('form').addEventListener('submit', () => {
  localStorage.removeItem('essay-draft');
});

Namespaced keys (avoid collisions)

const APP_PREFIX = 'myapp_v1_';

function appSet(key, value) {
  localStorage.setItem(APP_PREFIX + key, JSON.stringify(value));
}

function appGet(key, fallback = null) {
  const raw = localStorage.getItem(APP_PREFIX + key);
  return raw !== null ? JSON.parse(raw) : fallback;
}

Feature flag / onboarding

function hasSeenOnboarding() {
  return localStorage.getItem('onboarding_done') === 'true';
}

function markOnboardingDone() {
  localStorage.setItem('onboarding_done', 'true');
}

React hook: useLocalStorage

import { useState, useEffect } from 'react';

function useLocalStorage<T>(key: string, initialValue: T) {
  const [stored, setStored] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item !== null ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const toStore = value instanceof Function ? value(stored) : value;
    setStored(toStore);
    try {
      localStorage.setItem(key, JSON.stringify(toStore));
    } catch (err) {
      console.warn('localStorage write failed', err);
    }
  };

  return [stored, setValue] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');

Security considerations

localStorage is accessible by any JavaScript running on the same origin — including third-party scripts, browser extensions, and code injected via XSS.

Do NOT store:

  • Passwords or password hashes
  • Authentication tokens (prefer HttpOnly cookies)
  • Credit card numbers or PII
  • Session secrets

What's OK:

  • UI preferences (theme, language, layout)
  • Draft content (that isn't sensitive)
  • Non-secret user settings
  • Cache of public API responses

If you're storing JWT tokens, consider using short-lived tokens + refresh via HttpOnly cookie instead. localStorage is vulnerable to XSS; HttpOnly cookies are not.


6 common mistakes

Mistake Problem Fix
localStorage.setItem('user', user) Stores "[object Object]" Use JSON.stringify(user)
JSON.parse(localStorage.getItem('x')) Throws if getItem returns null Check for null first
Storing JWTs or passwords XSS can read them Use HttpOnly cookies for secrets
No try/catch around setItem Throws QuotaExceededError silently Wrap in try/catch
localStorage.getItem('count') + 1 String concatenation: "51" not 6 Parse: Number(localStorage.getItem('count')) + 1
Expecting storage event in the same tab Event only fires in other tabs Use a state variable or custom event

6 FAQ

Is localStorage available in all browsers?
Yes — all modern browsers and IE8+. In Node.js/SSR environments it doesn't exist; guard with typeof localStorage !== 'undefined' or use a package like localstorage-memory.

What's the storage limit?
Typically 5–10 MB per origin (varies by browser). Chrome and Firefox give ~5–10 MB; Safari gives ~5 MB. Large data (images, videos) should use IndexedDB instead.

Does localStorage work in incognito/private mode?
Yes, but data is cleared when the private session ends — behaving more like sessionStorage.

How is localStorage different from cookies?
Cookies are sent to the server with every request and can be HttpOnly (not accessible from JS). localStorage stays in the browser only, holds more data, and has a simpler API. Use cookies for authentication; use localStorage for client-side preferences.

Can different tabs share the same localStorage?
Yes — all tabs on the same origin (scheme + host + port) share the same localStorage. Changes in one tab fire the storage event in all others.

When should I use IndexedDB instead?
When you need to store more than ~5 MB, store binary data (Blobs, ArrayBuffers), or run complex queries. IndexedDB is asynchronous and far more powerful — but also more complex. For simple key-value preferences, localStorage is simpler and synchronous.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools