Browsers provide several mechanisms to store data on the client. Each has different lifetime, capacity, and security properties — choosing the wrong one leads to data loss, security vulnerabilities, or unnecessary network overhead.

Comparison

Feature localStorage sessionStorage Cookies
Lifetime Until explicitly cleared Tab/window session Configurable expiry
Size limit ~5–10 MB per origin ~5–10 MB per origin ~4 KB total
Sent to server No No Yes (every HTTP request)
Accessible from JS Yes Yes Yes (unless HttpOnly)
API complexity Simple key-value Simple key-value Manual string parsing

localStorage

Data persists across browser sessions until the user clears site data or your code removes it.

  // Set values (always strings)
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ name: 'Alice', id: 42 }));

// Get values
let theme = localStorage.getItem('theme');           // 'dark'
let user = JSON.parse(localStorage.getItem('user')); // { name: 'Alice', id: 42 }

// Check existence
if (localStorage.getItem('token') === null) {
    console.log('Not logged in');
}

// Remove specific key or all keys
localStorage.removeItem('theme');
localStorage.clear(); // removes ALL keys for this origin

// Iterate all keys
for (let i = 0; i < localStorage.length; i++) {
    let key = localStorage.key(i);
    console.log(key, localStorage.getItem(key));
}
  

Storage Wrapper Pattern

Because localStorage only stores strings, wrap it for type safety:

  const storage = {
    get(key, defaultValue = null) {
        const raw = localStorage.getItem(key);
        if (raw === null) return defaultValue;
        try { return JSON.parse(raw); }
        catch { return raw; }
    },
    set(key, value) {
        localStorage.setItem(key, JSON.stringify(value));
    },
    remove(key) {
        localStorage.removeItem(key);
    }
};

storage.set('preferences', { fontSize: 16, lang: 'en' });
storage.get('preferences'); // { fontSize: 16, lang: 'en' }
  

sessionStorage

Identical API to localStorage, but scoped to the current tab. Data is cleared when the tab closes.

  // Save form draft so refresh doesn't lose work
sessionStorage.setItem('formDraft', JSON.stringify({
    title: document.querySelector('#title').value,
    body: document.querySelector('#body').value
}));

// Restore on page load
const draft = JSON.parse(sessionStorage.getItem('formDraft'));
if (draft) {
    document.querySelector('#title').value = draft.title;
    document.querySelector('#body').value = draft.body;
}
  

Use sessionStorage for: multi-step wizards, temporary form state, data that should not survive tab close.

Storage Events (Cross-Tab Sync)

The storage event fires in other tabs when localStorage changes — not in the tab that made the change:

  window.addEventListener('storage', (e) => {
    console.log('Key changed:', e.key);
    console.log('Old value:', e.oldValue);
    console.log('New value:', e.newValue);

    if (e.key === 'theme') {
        applyTheme(e.newValue);
    }
});
  

Use this to sync theme or logout state across tabs.

Cookies

Cookies are small name-value pairs sent with every HTTP request to the matching domain. They are the traditional mechanism for session management.

  // Set a cookie (client-side)
document.cookie = 'username=Alice; max-age=3600; path=/; SameSite=Strict; Secure';

// Read all cookies (returns one semicolon-separated string)
console.log(document.cookie); // 'username=Alice; session=abc123'

// Parse a specific cookie
function getCookie(name) {
    return document.cookie
        .split('; ')
        .find(row => row.startsWith(name + '='))
        ?.split('=')[1];
}

console.log(getCookie('username')); // 'Alice'
  
Attribute Purpose
HttpOnly Not accessible via JavaScript (set server-side only)
Secure Sent only over HTTPS
SameSite=Strict Not sent on cross-site requests (CSRF protection)
max-age / expires Control lifetime

Modern authentication should use HttpOnly, Secure, SameSite cookies set by the server — never store JWTs in localStorage.

Practical Example: Theme Toggle

  const THEME_KEY = 'app-theme';

function getTheme() {
    return localStorage.getItem(THEME_KEY) || 'light';
}

function setTheme(theme) {
    localStorage.setItem(THEME_KEY, theme);
    document.documentElement.dataset.theme = theme;
}

// Apply saved theme on load
setTheme(getTheme());

document.querySelector('#toggle-theme').addEventListener('click', () => {
    setTheme(getTheme() === 'light' ? 'dark' : 'light');
});
  

Security Best Practices

  • Never store auth tokens in localStorage — any XSS attack can read them. Use HttpOnly cookies instead.
  • Validate data read from storage — treat it as untrusted input; schema may have changed.
  • Don’t store PII unnecessarily — minimize data at rest on the client.
  • Use SameSite=Strict on cookies to mitigate CSRF attacks.
  • Set short expiry on sensitive session cookies.

Troubleshooting

Problem Cause Solution
QuotaExceededError Storage full (~5 MB) Remove old keys; compress data; use IndexedDB for large data
Data lost after browser update User cleared site data Re-fetch from server; don’t rely solely on client storage
storage event not firing Same tab made the change Expected behavior — use CustomEvent for same-tab sync
Cookie not sent to API Missing path=/ or wrong domain Set cookie attributes to match your API domain
JSON.parse throws Corrupted or manually edited value Wrap in try/catch; reset to default

When to Use What

Use Case Recommended Storage
User theme / language preference localStorage
Shopping cart (non-sensitive) localStorage or sessionStorage
Multi-step form draft sessionStorage
Authentication session HttpOnly Secure cookie (server-set)
Large offline datasets IndexedDB
Analytics tracking ID Cookie (with consent)

Browser storage is ideal for user preferences, UI state, and non-sensitive cached data — but server-side storage remains the source of truth for anything important.