Web Storage API Guide: localStorage & sessionStorage Best Practices
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:
- localStorage: Data persists across browser sessions. It survives page reloads, browser restarts, and system reboots. Shared across all tabs of the same origin.
- sessionStorage: Data is scoped to the page session. It survives page reloads but is cleared when the tab or window is closed. Isolated per tab — different tabs have separate sessionStorage.
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:
- Chrome/Edge: ~10MB per origin
- Firefox: ~10MB per origin
- Safari: ~5MB per origin (can prompt user for more)
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
- Never store sensitive data: Passwords, API keys, JWT tokens, credit card numbers, and personal information should never go in localStorage. Use HttpOnly cookies or secure server-side sessions instead.
- Be aware of XSS: Any JavaScript running on your page (including third-party scripts) can read and write localStorage. Sanitize user input and use Content Security Policy to mitigate XSS.
- Clear storage on logout: When users log out, clear any stored authentication data and personal information.
- Don't trust stored data: Validate and sanitize any data read from localStorage before using it. A malicious browser extension could have modified it.
- Use sessionStorage for sensitive ephemeral data: Data that should not persist across sessions (like one-time tokens) belongs in sessionStorage.
Common Pitfalls
- Forgetting JSON.parse/stringify: Storing an object without serialization results in the string
"[object Object]". - Ignoring quota limits: Always wrap
setItemin try/catch to handleQuotaExceededError. - Blocking the main thread: For large datasets, consider using IndexedDB instead — localStorage is synchronous and can cause jank.
- Assuming storage is available: In private browsing modes or when storage is disabled, operations may throw. Always test availability.
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
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.
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.
No. localStorage is accessible to any JavaScript on the same origin, including XSS attacks. Never store passwords, tokens, or personal information in localStorage.
localStorage only stores strings. Use JSON.stringify() to convert objects before storing and JSON.parse() to convert them back when reading.
No. localStorage is scoped to the origin (protocol + domain + port). Different subdomains like app.example.com and blog.example.com have separate storage.