JavaScript LocalStorage and SessionStorage Guide

The Web Storage API provides two mechanisms for storing data client-side: `localStorage` and `sessionStorage`. Unlike cookies, which are sent with every HTTP request, Web Storage data stays in the...

Key Insights

  • LocalStorage persists data indefinitely across browser sessions while SessionStorage clears when the tab closes—choose based on your data’s intended lifespan, not convenience
  • Always wrap JSON.parse() in try-catch blocks when retrieving stored data; corrupted or manually-edited storage values will crash your application otherwise
  • Never store sensitive data like tokens or passwords in Web Storage—it’s accessible to any JavaScript on your page and vulnerable to XSS attacks

Understanding the Web Storage API

The Web Storage API provides two mechanisms for storing data client-side: localStorage and sessionStorage. Unlike cookies, which are sent with every HTTP request, Web Storage data stays in the browser, making it ideal for client-side state management without network overhead.

Here’s what sets them apart from cookies: Web Storage offers 5-10MB of space per origin (compared to 4KB for cookies), a cleaner API, and no automatic transmission to servers. The data persists purely client-side, accessible only through JavaScript.

Both storage mechanisms share identical APIs but differ fundamentally in lifecycle. LocalStorage persists until explicitly cleared—surviving browser restarts, crashes, and system reboots. SessionStorage lives only for the duration of the page session, clearing when you close the tab or window.

Browser support is universal across modern browsers. Even IE8 supported these APIs, so you can use them without polyfills in any realistic production environment.

LocalStorage: Persistent Client-Side Storage

LocalStorage provides simple key-value storage that persists indefinitely. The API is straightforward:

// Store data
localStorage.setItem('username', 'alice');
localStorage.setItem('theme', 'dark');

// Retrieve data
const username = localStorage.getItem('username'); // 'alice'
const theme = localStorage.getItem('theme'); // 'dark'

// Check if key exists
if (localStorage.getItem('username') !== null) {
  console.log('User is logged in');
}

// Remove specific item
localStorage.removeItem('theme');

// Clear all data
localStorage.clear();

Keys and values are always stored as strings. If you store a number, it’s converted automatically:

localStorage.setItem('count', 42);
const count = localStorage.getItem('count'); // '42' (string!)
const numericCount = parseInt(localStorage.getItem('count'), 10); // 42 (number)

You can also use property access syntax, though setItem() and getItem() are more explicit:

localStorage.username = 'alice'; // Works, but less clear
const user = localStorage.username; // Also works

SessionStorage: Tab-Scoped Temporary Storage

SessionStorage uses the exact same API as localStorage but with completely different persistence behavior:

// Store session data
sessionStorage.setItem('formDraft', 'User input...');
sessionStorage.setItem('currentStep', '3');

// Retrieve session data
const draft = sessionStorage.getItem('formDraft');
const step = sessionStorage.getItem('currentStep');

The critical difference: sessionStorage is scoped to the browser tab. Open the same URL in two tabs, and each has its own isolated sessionStorage. Close the tab, and the data vanishes.

Here’s a practical demonstration:

// In Tab 1
localStorage.setItem('persistent', 'I survive');
sessionStorage.setItem('temporary', 'I do not');

// Open Tab 2 with same origin
console.log(localStorage.getItem('persistent')); // 'I survive'
console.log(sessionStorage.getItem('temporary')); // null

// Close Tab 1 and reopen
console.log(localStorage.getItem('persistent')); // 'I survive'
console.log(sessionStorage.getItem('temporary')); // null

Use sessionStorage for wizard-style forms, temporary UI state, or any data that shouldn’t persist beyond the current browsing session.

Storing Complex Data Structures

Web Storage only handles strings, but you can store objects and arrays using JSON serialization:

// Storing objects
const userPreferences = {
  theme: 'dark',
  fontSize: 16,
  notifications: true,
  layout: ['sidebar', 'main', 'footer']
};

localStorage.setItem('preferences', JSON.stringify(userPreferences));

// Retrieving objects
const stored = localStorage.getItem('preferences');
const preferences = JSON.parse(stored);
console.log(preferences.theme); // 'dark'

Always handle JSON parsing errors. Users can manually edit localStorage through DevTools, corrupting your data:

function getStoredObject(key) {
  try {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : null;
  } catch (error) {
    console.error(`Failed to parse ${key}:`, error);
    localStorage.removeItem(key); // Clear corrupted data
    return null;
  }
}

// Usage
const preferences = getStoredObject('preferences') || {
  theme: 'light', // default fallback
  fontSize: 14
};

For arrays, the pattern is identical:

// Shopping cart example
const cart = [
  { id: 1, name: 'Widget', quantity: 2 },
  { id: 2, name: 'Gadget', quantity: 1 }
];

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

const storedCart = JSON.parse(localStorage.getItem('cart') || '[]');
storedCart.push({ id: 3, name: 'Doohickey', quantity: 1 });
localStorage.setItem('cart', JSON.stringify(storedCart));

Practical Use Cases and Implementation Patterns

Auto-saving form drafts prevents users from losing work:

const form = document.querySelector('#article-form');
const DRAFT_KEY = 'article-draft';

// Auto-save on input
form.addEventListener('input', () => {
  const formData = {
    title: form.title.value,
    content: form.content.value,
    timestamp: Date.now()
  };
  localStorage.setItem(DRAFT_KEY, JSON.stringify(formData));
});

// Restore on page load
window.addEventListener('DOMContentLoaded', () => {
  const draft = getStoredObject(DRAFT_KEY);
  if (draft) {
    form.title.value = draft.title || '';
    form.content.value = draft.content || '';
    console.log(`Draft restored from ${new Date(draft.timestamp)}`);
  }
});

// Clear after successful submission
form.addEventListener('submit', () => {
  localStorage.removeItem(DRAFT_KEY);
});

Theme persistence with immediate application:

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

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

// Apply on load
document.documentElement.setAttribute('data-theme', getTheme());

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

Simple API response caching:

async function fetchWithCache(url, ttl = 3600000) { // 1 hour default
  const cacheKey = `cache:${url}`;
  const cached = getStoredObject(cacheKey);
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    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 Considerations and Limitations

Never store sensitive data in Web Storage. It’s accessible to any JavaScript running on your page, making it vulnerable to XSS attacks:

// NEVER do this
localStorage.setItem('authToken', 'secret-jwt-token'); // BAD!
localStorage.setItem('password', 'user-password'); // TERRIBLE!
localStorage.setItem('creditCard', '4111-1111-1111-1111'); // CATASTROPHIC!

// Use httpOnly cookies for sensitive data instead

Storage quotas vary by browser (typically 5-10MB). Handle quota exceeded errors:

function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (error) {
    if (error.name === 'QuotaExceededError') {
      console.warn('Storage quota exceeded');
      // Strategy: clear old cache entries
      clearOldCacheEntries();
      // Retry once
      try {
        localStorage.setItem(key, value);
        return true;
      } catch (retryError) {
        console.error('Still out of space after cleanup');
        return false;
      }
    }
    throw error;
  }
}

function clearOldCacheEntries() {
  const keys = Object.keys(localStorage);
  keys.filter(k => k.startsWith('cache:'))
      .forEach(k => localStorage.removeItem(k));
}

Web Storage is bound by same-origin policy. https://example.com cannot access storage from https://api.example.com or http://example.com.

Best Practices and Helper Utilities

Create a wrapper utility for type-safe storage with expiration support:

class Storage {
  constructor(storage = localStorage) {
    this.storage = storage;
  }
  
  set(key, value, ttl = null) {
    const item = {
      value,
      expiry: ttl ? Date.now() + ttl : null
    };
    
    try {
      this.storage.setItem(key, JSON.stringify(item));
      return true;
    } catch (error) {
      console.error('Storage set failed:', error);
      return false;
    }
  }
  
  get(key, defaultValue = null) {
    try {
      const itemStr = this.storage.getItem(key);
      if (!itemStr) return defaultValue;
      
      const item = JSON.parse(itemStr);
      
      // Check expiration
      if (item.expiry && Date.now() > item.expiry) {
        this.storage.removeItem(key);
        return defaultValue;
      }
      
      return item.value;
    } catch (error) {
      console.error('Storage get failed:', error);
      this.storage.removeItem(key);
      return defaultValue;
    }
  }
  
  remove(key) {
    this.storage.removeItem(key);
  }
  
  clear() {
    this.storage.clear();
  }
}

// Usage
const store = new Storage(localStorage);
store.set('user', { name: 'Alice' }, 3600000); // 1 hour TTL
const user = store.get('user', { name: 'Guest' });

Always feature-detect before using Web Storage:

function storageAvailable(type) {
  try {
    const storage = window[type];
    const test = '__storage_test__';
    storage.setItem(test, test);
    storage.removeItem(test);
    return true;
  } catch (error) {
    return false;
  }
}

if (storageAvailable('localStorage')) {
  // Use localStorage
} else {
  // Fallback to in-memory storage or cookies
}

Web Storage is a powerful tool for client-side state management when used appropriately. Keep data non-sensitive, handle errors gracefully, and always consider the lifecycle requirements of your data when choosing between localStorage and sessionStorage.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.