JavaScript localStorage & sessionStorage in 2026: The Complete Guide
Table of Contents
- What is Web Storage?
- localStorage vs sessionStorage
- Basic CRUD Operations
- Storing Complex Data with JSON
- Storage Events & Cross-Tab Communication
- Building a Storage Wrapper Class
- Implementing Expiring Data
- Storage Quota & Limits
- Security Considerations
- Practical Storage Patterns
- Common Mistakes to Avoid
- Conclusion
JavaScript localStorage and sessionStorage are the simplest ways to persist data in the browser. Whether you’re saving user preferences, caching API responses, or implementing a shopping cart, these Web Storage APIs give you synchronous key-value storage without any server involvement.
In this guide, you’ll learn everything about browser storage — from basic operations to advanced patterns like cross-tab communication, expiring data, and building type-safe storage wrappers. By the end, you’ll know exactly when to use localStorage vs sessionStorage and how to avoid the common pitfalls that trip up most developers.
What is Web Storage?
Web Storage is a browser API that lets JavaScript store key-value pairs locally. Unlike cookies (which are sent with every HTTP request), Web Storage data stays on the client and is never automatically transmitted to the server. The API provides two storage objects: localStorage for persistent data and sessionStorage for session-scoped data.
// Both have the same API
localStorage.setItem("theme", "dark");
sessionStorage.setItem("currentStep", "3");
// Data persists differently
// localStorage: survives browser restarts
// sessionStorage: cleared when tab closes
Both storage objects share the same interface. The only difference is their lifetime and scope. Every origin (protocol + domain + port) gets its own isolated storage — https://example.com cannot read storage set by https://other.com.
localStorage vs sessionStorage
Understanding when to use each storage type is crucial. Here’s a clear comparison.
localStorage persists until explicitly cleared. Data survives browser restarts, system reboots, and tab closures. All tabs and windows from the same origin share the same localStorage data. Use it for user preferences, saved state, and cached data that should persist.
sessionStorage lasts only for the current browser tab session. Each tab gets its own sessionStorage, even if both tabs have the same URL. When the tab closes, the data is gone. Use it for temporary state like form progress, wizard steps, or per-tab navigation history.
// localStorage — shared across all tabs, persists forever
localStorage.setItem("username", "Alice");
// Open new tab to same site — "Alice" is available
// sessionStorage — per-tab, dies with the tab
sessionStorage.setItem("scrollPosition", "450");
// Open new tab — scrollPosition is NOT available there
A practical rule of thumb: if you’d be annoyed to lose the data when closing a tab, use localStorage. If the data only makes sense for the current browsing session or tab, use sessionStorage.
Basic CRUD Operations
The Web Storage API is intentionally simple — just five core methods plus a length property.
// CREATE / UPDATE — setItem(key, value)
localStorage.setItem("name", "Alice");
localStorage.setItem("name", "Bob"); // overwrites
// READ — getItem(key)
const name = localStorage.getItem("name"); // "Bob"
const missing = localStorage.getItem("nonexistent"); // null
// DELETE — removeItem(key)
localStorage.removeItem("name");
// DELETE ALL — clear()
localStorage.clear(); // removes everything for this origin
// COUNT — length property
console.log(localStorage.length); // number of stored keys
// ITERATE — key(index)
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
Important: both keys and values must be strings. If you pass a non-string value, it's automatically converted via toString(). This means localStorage.setItem("count", 42) stores the string "42", and localStorage.setItem("data", {a: 1}) stores "[object Object]" — which is almost certainly not what you want. We'll solve this with JSON in the next section.
Storing Complex Data with JSON
Since Web Storage only accepts strings, you need JSON serialization for any complex data type — objects, arrays, numbers, booleans, and nested structures.
// Storing complex data
const user = {
name: "Alice",
preferences: {
theme: "dark",
fontSize: 16,
notifications: true
},
recentSearches: ["proxy", "reflect", "modules"]
};
localStorage.setItem("user", JSON.stringify(user));
// Retrieving complex data
const stored = localStorage.getItem("user");
const parsed = stored ? JSON.parse(stored) : null;
console.log(parsed.preferences.theme); // "dark"
Here's a helper function that handles JSON serialization and error recovery automatically:
function storageGet(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item !== null ? JSON.parse(item) : defaultValue;
} catch {
// If JSON.parse fails, return the raw string
return localStorage.getItem(key) ?? defaultValue;
}
}
function storageSet(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
if (e.name === "QuotaExceededError") {
console.error("Storage quota exceeded!");
}
return false;
}
}
// Usage
storageSet("settings", { theme: "dark", lang: "en" });
const settings = storageGet("settings", { theme: "light" });
console.log(settings.theme); // "dark"
Storage Events & Cross-Tab Communication
One of localStorage's most powerful features is the storage event. When localStorage is modified in one tab, all other tabs from the same origin receive a storage event. This enables real-time cross-tab communication without WebSockets or servers.
// Tab A: Listen for changes from other tabs
window.addEventListener("storage", (event) => {
console.log("Storage changed!");
console.log("Key:", event.key);
console.log("Old value:", event.oldValue);
console.log("New value:", event.newValue);
console.log("URL of changer:", event.url);
if (event.key === "theme") {
applyTheme(event.newValue);
}
});
// Tab B: Make a change (Tab A's listener fires)
localStorage.setItem("theme", "dark");
Important: the storage event fires only in other tabs, not the tab that made the change. This is by design — you already know about changes you initiated. The event object provides the key, old value, new value, and the URL of the tab that made the change.
Here's a practical cross-tab notification system using storage events:
// Cross-tab messaging system
function broadcastMessage(channel, data) {
const message = JSON.stringify({
data,
timestamp: Date.now(),
sender: sessionStorage.getItem("tabId")
});
localStorage.setItem(`msg_${channel}`, message);
// Clean up immediately — we only care about the event
localStorage.removeItem(`msg_${channel}`);
}
function onMessage(channel, callback) {
window.addEventListener("storage", (event) => {
if (event.key === `msg_${channel}` && event.newValue) {
const message = JSON.parse(event.newValue);
callback(message.data, message);
}
});
}
// Tab A
onMessage("auth", (data) => {
if (data.action === "logout") {
window.location.href = "/login";
}
});
// Tab B — broadcast logout to all tabs
broadcastMessage("auth", { action: "logout" });
Building a Storage Wrapper Class
For production applications, wrapping the raw Storage API in a class gives you JSON handling, namespacing, and type safety in one place.
class StorageManager {
#prefix;
#storage;
constructor(prefix = "app", useSession = false) {
this.#prefix = prefix;
this.#storage = useSession ? sessionStorage : localStorage;
}
#key(name) {
return `${this.#prefix}:${name}`;
}
get(key, defaultValue = null) {
try {
const raw = this.#storage.getItem(this.#key(key));
if (raw === null) return defaultValue;
return JSON.parse(raw);
} catch {
return defaultValue;
}
}
set(key, value) {
try {
this.#storage.setItem(this.#key(key), JSON.stringify(value));
return true;
} catch {
return false;
}
}
remove(key) {
this.#storage.removeItem(this.#key(key));
}
clear() {
// Only clear items with our prefix
const keysToRemove = [];
for (let i = 0; i < this.#storage.length; i++) {
const key = this.#storage.key(i);
if (key.startsWith(this.#prefix + ":")) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(k => this.#storage.removeItem(k));
}
keys() {
const result = [];
const prefixLen = this.#prefix.length + 1;
for (let i = 0; i < this.#storage.length; i++) {
const key = this.#storage.key(i);
if (key.startsWith(this.#prefix + ":")) {
result.push(key.slice(prefixLen));
}
}
return result;
}
}
// Usage
const store = new StorageManager("myApp");
store.set("user", { name: "Alice", role: "admin" });
store.set("settings", { theme: "dark" });
console.log(store.get("user")); // { name: "Alice", role: "admin" }
console.log(store.keys()); // ["user", "settings"]
store.clear(); // Only clears myApp:* keys
Implementing Expiring Data
localStorage doesn't support TTL (time-to-live) natively. Here's how to implement auto-expiring storage entries.
function setWithExpiry(key, value, ttlMs) {
const item = {
value,
expiry: Date.now() + ttlMs
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const item = JSON.parse(raw);
if (!item.expiry) {
return item; // Legacy item without expiry
}
if (Date.now() > item.expiry) {
localStorage.removeItem(key); // Clean up expired item
return null;
}
return item.value;
} catch {
return null;
}
}
// Cache API response for 5 minutes
const cachedData = getWithExpiry("apiCache");
if (cachedData) {
renderData(cachedData);
} else {
fetch("/api/data")
.then(r => r.json())
.then(data => {
setWithExpiry("apiCache", data, 5 * 60 * 1000);
renderData(data);
});
}
This pattern works great for caching Fetch API responses. The first request stores the result with a TTL. Subsequent page loads use the cached version until it expires, then fetch fresh data.
Storage Quota & Limits
Every browser limits Web Storage to approximately 5-10 MB per origin. The exact limit varies by browser: Chrome and Firefox allow about 5 MB, Safari about 5 MB for localStorage and unlimited for sessionStorage (up to available memory).
// Check how much storage is used
function getStorageSize(storage = localStorage) {
let totalBytes = 0;
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
// Each character is 2 bytes in JavaScript (UTF-16)
totalBytes += (key.length + value.length) * 2;
}
return {
bytes: totalBytes,
kb: (totalBytes / 1024).toFixed(2),
mb: (totalBytes / (1024 * 1024)).toFixed(2)
};
}
console.log(getStorageSize());
// { bytes: 2048, kb: "2.00", mb: "0.00" }
// Test if storage is available
function isStorageAvailable(type = "localStorage") {
try {
const storage = window[type];
const testKey = "__storage_test__";
storage.setItem(testKey, "test");
storage.removeItem(testKey);
return true;
} catch {
return false;
}
}
if (!isStorageAvailable()) {
console.warn("localStorage not available — using in-memory fallback");
}
When storage is full, setItem() throws a QuotaExceededError. Always wrap writes in try-catch and have a fallback strategy. Common approaches include removing the oldest items, clearing cached data first, or falling back to in-memory storage. Handle this gracefully with proper error handling.
Security Considerations
Web Storage has significant security limitations you must understand before storing sensitive data.
Never store sensitive data in localStorage. Any JavaScript running on your page can read localStorage — including third-party scripts, browser extensions, and XSS attack payloads. Tokens, passwords, credit card numbers, and personal identification numbers should never be in localStorage.
// DANGEROUS — never do this
localStorage.setItem("authToken", "eyJhbG...");
localStorage.setItem("password", "secret123");
// SAFER alternatives for auth tokens:
// - HttpOnly cookies (not accessible via JavaScript)
// - sessionStorage (if you must use JS storage)
// - In-memory variables (cleared on page refresh)
XSS attacks can steal all localStorage data. If an attacker injects JavaScript into your page, they can run JSON.stringify(localStorage) and exfiltrate everything. This is why Content Security Policy headers and input sanitization are critical.
Data is not encrypted. localStorage stores data as plain text. Anyone with physical access to the device can read it through browser DevTools. For sensitive preferences, consider encrypting values before storing them.
Practical Storage Patterns
Dark Mode Persistence
// Save theme preference
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}
// Apply saved theme on page load (put in <head> to prevent flash)
const savedTheme = localStorage.getItem("theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark" : "light");
document.documentElement.setAttribute("data-theme", savedTheme);
Form Draft Auto-Save
const form = document.querySelector("#contactForm");
const DRAFT_KEY = "contactDraft";
// Restore draft on page load
const draft = JSON.parse(localStorage.getItem(DRAFT_KEY) || "{}");
Object.entries(draft).forEach(([name, value]) => {
const field = form.elements[name];
if (field) field.value = value;
});
// Auto-save on input
form.addEventListener("input", (e) => {
const current = JSON.parse(localStorage.getItem(DRAFT_KEY) || "{}");
current[e.target.name] = e.target.value;
localStorage.setItem(DRAFT_KEY, JSON.stringify(current));
});
// Clear draft on successful submit
form.addEventListener("submit", () => {
localStorage.removeItem(DRAFT_KEY);
});
Recently Viewed Items
function addRecentItem(item, maxItems = 10) {
const recent = JSON.parse(localStorage.getItem("recent") || "[]");
// Remove duplicate if exists
const filtered = recent.filter(r => r.id !== item.id);
// Add to front
filtered.unshift({ ...item, viewedAt: Date.now() });
// Trim to max
const trimmed = filtered.slice(0, maxItems);
localStorage.setItem("recent", JSON.stringify(trimmed));
return trimmed;
}
function getRecentItems() {
return JSON.parse(localStorage.getItem("recent") || "[]");
}
Common Mistakes to Avoid
The biggest mistake is storing objects without JSON.stringify(). The code localStorage.setItem("user", userObj) stores "[object Object]" — completely useless. Always serialize with JSON.
Another common error is assuming localStorage is always available. Private browsing modes in some browsers restrict or disable Web Storage entirely. Safari's private mode used to throw on any setItem call. Always test availability before relying on it.
Don't use localStorage for large datasets. With a 5 MB limit, you can run out faster than you'd think — especially when storing JSON strings, which add overhead for keys, quotes, and escape characters. For larger storage needs, look into IndexedDB.
Conclusion
JavaScript localStorage and sessionStorage provide a simple, synchronous way to persist data in the browser. You've learned the difference between persistent and session storage, how to handle complex data with JSON, cross-tab communication via storage events, building robust wrapper classes, implementing TTL-based expiration, and critical security considerations.
The key rule: use localStorage for data that should survive page reloads and browser restarts, sessionStorage for data scoped to a single tab session, and never store sensitive information in either. For anything beyond simple key-value pairs, consider IndexedDB for more capable client-side storage.
Next up, we'll dive into JavaScript timers — setTimeout and setInterval for controlling timing in your code.