Web Storage API Guide: localStorage & sessionStorage Best Practices

Published April 2026 · JavaScript Tutorial · Try JSON Formatter →

The Web Storage API provides two simple mechanisms — localStorage and sessionStorage — for storing key-value pairs in the browser. Unlike cookies, Web Storage does not send data to the server with every request, offers significantly more space (5-10MB vs 4KB), and has a cleaner API. It is ideal for persisting user preferences, caching API responses, saving form drafts, and maintaining client-side application state.

This guide covers the complete API, practical patterns, security considerations, and common pitfalls to avoid.

localStorage vs sessionStorage

Both share the same API but differ in scope and lifetime:

Choose localStorage for data that should persist (user preferences, saved settings, cached data). Choose sessionStorage for temporary data (form state, one-time flags, step-by-step wizard progress).

Basic CRUD Operations

The API is minimal — just four methods and one property:

// === CREATE / UPDATE ===
localStorage.setItem('username', 'Alice');
localStorage.setItem('theme', 'dark');
localStorage.setItem('fontSize', '16');

// === READ ===
const username = localStorage.getItem('username'); // 'Alice'
const missing = localStorage.getItem('nonexistent'); // null

// === UPDATE ===
localStorage.setItem('theme', 'light'); // Overwrites 'dark'

// === DELETE ===
localStorage.removeItem('username'); // Delete one item
localStorage.clear();               // Delete everything

// === CHECK ===
console.log(localStorage.length);              // Number of stored items
console.log(localStorage.key(0));             // Key name at index 0
console.log('theme' in localStorage);         // Check if key exists

The same API works with sessionStorage — just replace localStorage with sessionStorage.

Storing and Retrieving Objects

Web Storage only stores strings. To store objects or arrays, serialize them with JSON.stringify() and parse them back with JSON.parse().

// Store an object
const settings = {
  theme: 'dark',
  fontSize: 16,
  language: 'en',
  notifications: true,
};

localStorage.setItem('settings', JSON.stringify(settings));

// Retrieve and parse
const stored = localStorage.getItem('settings');
const settings = stored ? JSON.parse(stored) : null;

console.log(settings.theme); // 'dark'

Always handle the case where getItem returns null (key doesn't exist) or where JSON.parse might throw an error (corrupted data).

Safe Storage Helper Class

Here is a robust wrapper that handles errors, JSON serialization, and type safety:

class SafeStorage {
  constructor(storage) {
    this.storage = storage;
  }

  set(key, value) {
    try {
      this.storage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      if (error.name === 'QuotaExceededError') {
        console.error('Storage full:', key);
      } else {
        console.error('Storage error:', error);
      }
      return false;
    }
  }

  get(key, defaultValue = null) {
    try {
      const item = this.storage.getItem(key);
      return item !== null ? JSON.parse(item) : defaultValue;
    } catch {
      return defaultValue;
    }
  }

  remove(key) {
    this.storage.removeItem(key);
  }

  clear() {
    this.storage.clear();
  }

  has(key) {
    return this.storage.getItem(key) !== null;
  }
}

// Usage
const store = new SafeStorage(localStorage);
store.set('user', { name: 'Alice', age: 30 });
const user = store.get('user', { name: 'Guest' });

Storage Limits and Quota Management

Storage limits vary by browser:

Check available space and handle quota errors:

function getStorageUsage() {
  let total = 0;
  for (let key in localStorage) {
    if (localStorage.hasOwnProperty(key)) {
      total += localStorage[key].length * 2; // UTF-16 = 2 bytes per char
    }
  }
  const mb = (total / 1024 / 1024).toFixed(2);
  console.log(`Storage used: ${mb} MB`);
  return total;
}

function estimateRemainingSpace() {
  const testKey = '__storage_test__';
  const data = 'x'.repeat(1024); // 1KB string
  let count = 0;

  try {
    while (true) {
      localStorage.setItem(testKey + count, data);
      count++;
    }
  } catch (error) {
    // Clean up test data
    for (let i = 0; i < count; i++) {
      localStorage.removeItem(testKey + i);
    }
    console.log(`Approximate remaining: ${(count / 1024).toFixed(2)} MB`);
  }
}

Listening for Storage Changes

The storage event fires when localStorage is modified in another tab. This enables cross-tab communication and synchronization.

window.addEventListener('storage', (event) => {
  console.log('Key changed:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('Changed by:', event.url);
});

Important: The storage event does NOT fire in the tab that made the change. It only fires in other tabs sharing the same origin. This makes it perfect for synchronizing state across tabs.

Common Use Cases

User Preferences

// Save theme preference
function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('theme', theme);
}

// Load theme on page load
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
  document.documentElement.setAttribute('data-theme', savedTheme);
}

Form Draft Autosave

const form = document.querySelector('#compose-form');
const STORAGE_KEY = 'form-draft';

// Autosave on input
form.addEventListener('input', () => {
  const formData = new FormData(form);
  const data = Object.fromEntries(formData.entries());
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
});

// Restore draft on page load
const draft = sessionStorage.getItem(STORAGE_KEY);
if (draft) {
  const data = JSON.parse(draft);
  Object.entries(data).forEach(([key, value]) => {
    const field = form.querySelector(`[name="${key}"]`);
    if (field) field.value = value;
  });
}

// Clear on submit
form.addEventListener('submit', () => {
  sessionStorage.removeItem(STORAGE_KEY);
});

Caching API Responses

async function fetchWithCache(url, cacheTime = 300000) {
  const cacheKey = `cache_${url}`;
  const cached = localStorage.getItem(cacheKey);

  if (cached) {
    const { data, timestamp } = JSON.parse(cached);
    if (Date.now() - timestamp < cacheTime) {
      return data; // Return cached data
    }
  }

  const response = await fetch(url);
  const data = await response.json();

  localStorage.setItem(cacheKey, JSON.stringify({
    data,
    timestamp: Date.now(),
  }));

  return data;
}

Security Best Practices

Common Pitfalls

When to Use IndexedDB Instead

Use IndexedDB when you need to store more than 5-10MB, work with structured data, perform complex queries, or need asynchronous access. IndexedDB is the browser's full database solution, while Web Storage is for simple key-value needs.

Frequently Asked Questions

What is the difference between localStorage and sessionStorage?

localStorage persists data across browser sessions and shares it across all tabs of the same origin. sessionStorage is scoped to a single tab and is cleared when the tab is closed.

How much data can I store in localStorage?

Most browsers allow 5-10MB per origin. Chrome provides about 10MB. You can check available space by trying to store data and catching the QuotaExceededError.

Is localStorage secure?

No. localStorage is accessible to any JavaScript on the same origin, including XSS attacks. Never store passwords, tokens, or personal information in localStorage.

Can I store objects in localStorage?

localStorage only stores strings. Use JSON.stringify() to convert objects before storing and JSON.parse() to convert them back when reading.

Does localStorage work across subdomains?

No. localStorage is scoped to the origin (protocol + domain + port). Different subdomains like app.example.com and blog.example.com have separate storage.