Storage: localStorage, sessionStorage, and IndexedDB

· 5 min read · Updated March 7, 2026 · beginner
storage localStorage sessionStorage IndexedDB browser

Every web application needs to store data somewhere. Whether you’re saving user preferences, caching API responses, or persisting application state, the browser provides several storage mechanisms to choose from. In this tutorial, you’ll learn about localStorage, sessionStorage, and IndexedDB—three powerful ways to store data client-side.

Why Browser Storage Matters

Before diving into the APIs, let’s understand why browser storage is essential:

  • Persist user preferences—themes, language settings, or UI state
  • Cache data—reduce server requests by storing API responses
  • Enable offline functionality— Progressive Web Apps (PWAs) rely on storage
  • Improve performance—store computed results locally

Each storage option has different characteristics. Let’s explore them.

localStorage: Simple Key-Value Storage

The localStorage API provides a simple interface for storing string data that persists across browser sessions. It’s synchronous and straightforward to use.

Writing Data

// Store a string value
localStorage.setItem('username', 'alice');
localStorage.setItem('theme', 'dark');

// Store JSON data (must stringify)
const user = { id: 1, name: 'Alice', role: 'admin' };
localStorage.setItem('user', JSON.stringify(user));

Reading Data

// Retrieve a string value
const username = localStorage.getItem('username');
console.log(username); // "alice"

// Retrieve JSON data (must parse)
const userData = JSON.parse(localStorage.getItem('user'));
console.log(userData.name); // "Alice"

// Handle missing values
const theme = localStorage.getItem('theme') || 'light';

Removing Data

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

// Clear all localStorage data
localStorage.clear();

Storage Limits and Caveats

localStorage has some important limitations:

  • Capacity: Typically 5-10 MB per origin
  • Type restriction: Only stores strings (hence JSON.stringify)
  • Synchronous: Blocks the main thread for large operations
  • Not secure: Data is visible in browser DevTools
// Check available storage space
const estimate = navigator.storage.estimate();
estimate.then(({ quota, usage }) => {
  console.log(`Using ${usage} of ${quota} bytes`);
});

sessionStorage: Session-Scoped Storage

The sessionStorage API works exactly like localStorage, but data is cleared when the browser tab or window closes. It’s perfect for temporary data that shouldn’t persist.

Basic Usage

// Set and get items (same API as localStorage)
sessionStorage.setItem('tempToken', 'abc123');
const token = sessionStorage.getItem('tempToken');

// Remove when done
sessionStorage.removeItem('tempToken');

Practical Example: Form Progress

sessionStorage is ideal for preserving form data during a session:

// Save form input as user types
document.querySelector('#comment').addEventListener('input', (e) => {
  sessionStorage.setItem('draftComment', e.target.value);
});

// Restore on page load
window.addEventListener('DOMContentLoaded', () => {
  const draft = sessionStorage.getItem('draftComment');
  if (draft) {
    document.querySelector('#comment').value = draft;
  }
});

// Clear on successful submission
document.querySelector('#comment-form').addEventListener('submit', () => {
  sessionStorage.removeItem('draftComment');
});

Key Difference from localStorage

// localStorage: persists after closing browser
localStorage.setItem('test', 'persistent');
// → Still available after closing and reopening browser

// sessionStorage: cleared when tab closes
sessionStorage.setItem('test', 'temporary');
// → Gone when tab/window closes

IndexedDB: Complex Data Storage

For large amounts of structured data, IndexedDB provides a powerful NoSQL-like database in the browser. Unlike localStorage, IndexedDB supports:

  • Large storage capacity (often hundreds of MB)
  • Complex data types (objects, blobs, files)
  • Asynchronous API with promises
  • Indexes for fast queries
  • Transactions for data integrity

Opening a Database

// Open or create a database
const request = indexedDB.open('MyAppDB', 1);

// Handle upgrades (schema changes)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // Create an object store
  if (!db.objectStoreNames.contains('users')) {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    
    // Create indexes for querying
    store.createIndex('email', 'email', { unique: true });
    store.createIndex('name', 'name', { unique: false });
  }
};

request.onsuccess = (event) => {
  console.log('Database opened successfully');
};

request.onerror = (event) => {
  console.error('Database error:', event.target.error);
};

Wrapped with Promises

Working with raw IndexedDB is verbose. Here’s a cleaner approach:

// Helper function to wrap IndexedDB operations
function dbOperation(dbName, storeName, mode) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, 1);
    
    request.onsuccess = (event) => {
      const db = event.target.result;
      const transaction = db.transaction(storeName, mode);
      const store = transaction.objectStore(storeName);
      resolve({ db, transaction, store });
    };
    
    request.onerror = () => reject(request.error);
  });
}

// Add a record
async function addUser(user) {
  const { store } = await dbOperation('MyAppDB', 'users', 'readwrite');
  return new Promise((resolve, reject) => {
    const request = store.add(user);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Get a record by key
async function getUser(id) {
  const { store } = await dbOperation('MyAppDB', 'users', 'readonly');
  return new Promise((resolve, reject) => {
    const request = store.get(id);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Query with index
async function getUserByEmail(email) {
  const { store } = await dbOperation('MyAppDB', 'users', 'readonly');
  const index = store.index('email');
  return new Promise((resolve, reject) => {
    const request = index.get(email);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Practical Example: Offline Data Sync

// Store API response for offline access
async function cacheUsers(users) {
  const { store } = await dbOperation('MyAppDB', 'users', 'readwrite');
  const transaction = store.transaction;
  
  users.forEach(user => store.put(user));
  
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve();
    transaction.onerror = () => reject(transaction.error);
  });
}

// Get cached users (works offline)
async function getCachedUsers() {
  const { store } = await dbOperation('MyAppDB', 'users', 'readonly');
  return new Promise((resolve, reject) => {
    const request = store.getAll();
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Choosing the Right Storage

FeaturelocalStoragesessionStorageIndexedDB
Capacity~5-10 MB~5-10 MB100+ MB
Data typesStrings onlyStrings onlyAny serializable
API styleSynchronousSynchronousAsynchronous
QueriesKey onlyKey onlyIndexes
PersistenceForeverSessionForever
Best forUser prefsTemp dataLarge/complex data

Quick Decision Guide

  • User preferences → localStorage
  • Form drafts, temp state → sessionStorage
  • Large datasets, offline cache → IndexedDB
  • Sensitive data → None of these (use server-side storage)

Storage Events

Both localStorage and sessionStorage fire events when data changes in other tabs or windows:

window.addEventListener('storage', (event) => {
  console.log('Storage changed:', {
    key: event.key,
    oldValue: event.oldValue,
    newValue: event.newValue,
    storageArea: event.storageArea
  });
});

// This event only fires in other tabs, not the originating tab

This is useful for synchronizing state across multiple browser tabs.

Summary

You now understand three fundamental browser storage mechanisms:

  1. localStorage — Simple, persistent key-value storage for small data
  2. sessionStorage — Temporary key-value storage scoped to a tab/window
  3. IndexedDB — Powerful NoSQL database for large, structured data

Each has its place in modern web applications. Start with localStorage for simple needs, reach for IndexedDB when you need scale, and use sessionStorage for ephemeral data.

In the next tutorial, we’ll explore the Fetch API for making network requests.