Web Storage & Cookie
Browsers provide several mechanisms for storing data on the client side. It is important to understand the characteristics of each approach and use them appropriately.
Differences Between localStorage and sessionStorage
Both APIs share the same interface but differ in their lifetime.
| Feature | localStorage | sessionStorage |
|---|---|---|
| Lifetime | Until explicitly deleted | Deleted when tab/window is closed |
| Scope | All tabs of the same origin | Current tab only |
| Capacity | 5~10MB | 5~10MB |
| Server transmission | None | None |
| Access | Synchronous API | Synchronous API |
// localStorage: persists even after the browser is closed
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
localStorage.clear(); // delete all items
localStorage.length; // number of stored items
localStorage.key(0); // access key by index
// sessionStorage: deleted when the tab is closed
sessionStorage.setItem('tempData', 'value');
sessionStorage.getItem('tempData');
// Iterate over all items
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(key, value);
}
// Alternative using Object.keys
Object.keys(localStorage).forEach(key => {
console.log(key, localStorage.getItem(key));
});
// storage event: detect changes from another tab (localStorage only)
window.addEventListener('storage', (e) => {
console.log('Key:', e.key);
console.log('Old value:', e.oldValue);
console.log('New value:', e.newValue);
console.log('Origin:', e.url);
console.log('Storage:', e.storageArea); // localStorage object
});
JSON Serialization/Deserialization Patterns
localStorage only stores strings, so objects and arrays must be converted to JSON.
// Basic pattern
const user = { name: 'Alice', age: 30, roles: ['admin', 'user'] };
// Save
localStorage.setItem('user', JSON.stringify(user));
// Read
const stored = localStorage.getItem('user');
const restoredUser = stored ? JSON.parse(stored) : null;
// Safe read helper
function getStorageItem(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item !== null ? JSON.parse(item) : defaultValue;
} catch (err) {
console.error(`Storage read failed (${key}):`, err);
return defaultValue;
}
}
function setStorageItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (err) {
// Handle QuotaExceededError
if (err.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
}
return false;
}
}
// Usage
setStorageItem('settings', { theme: 'dark', fontSize: 16 });
const settings = getStorageItem('settings', { theme: 'light', fontSize: 14 });
// Store with expiry time
function setWithExpiry(key, value, ttl) {
const item = {
value,
expiry: Date.now() + ttl,
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
// Usage (1-hour TTL)
setWithExpiry('accessToken', 'abc123', 60 * 60 * 1000);
const token = getWithExpiry('accessToken'); // null if expired
IndexedDB Basics
IndexedDB is a built-in NoSQL database in the browser. It can store large volumes of structured data and operates asynchronously.
| Feature | localStorage | IndexedDB |
|---|---|---|
| Capacity | 5~10MB | Hundreds of MB to GB |
| Data types | Strings only | Almost all types |
| Queries | Keys only | Indexes, range queries |
| Transactions | None | Supported |
| API | Synchronous | Asynchronous |
// IndexedDB Promise wrapper (basic usage)
function openDB(name, version, { upgrade } = {}) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
// Runs when the database structure changes
request.onupgradeneeded = (event) => {
const db = event.target.result;
upgrade?.(db, event.oldVersion, event.newVersion);
};
});
}
// Open DB and create stores
const db = await openDB('my-app', 1, {
upgrade(db) {
// Create ObjectStore (similar to a table)
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id', // unique key field
autoIncrement: false // whether to auto-increment
});
// Create indexes (for fast lookups)
store.createIndex('email', 'email', { unique: true });
store.createIndex('name', 'name', { unique: false });
}
if (!db.objectStoreNames.contains('posts')) {
const postsStore = db.createObjectStore('posts', {
keyPath: 'id',
autoIncrement: true
});
postsStore.createIndex('userId', 'userId', { unique: false });
postsStore.createIndex('createdAt', 'createdAt', { unique: false });
}
}
});
// Data manipulation helper
function transaction(db, storeNames, mode = 'readonly') {
const tx = db.transaction(storeNames, mode);
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(new Error('Transaction aborted'));
});
}
function idbRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// CRUD operations
async function addUser(db, user) {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
await idbRequest(store.add(user));
await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
async function getUser(db, id) {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
return idbRequest(store.get(id));
}
async function updateUser(db, user) {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
await idbRequest(store.put(user)); // put: updates if exists, adds if not
}
async function deleteUser(db, id) {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
await idbRequest(store.delete(id));
}
// Search by index
async function getUserByEmail(db, email) {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const index = store.index('email');
return idbRequest(index.get(email));
}
// Iterate all data with a cursor
async function getAllUsers(db) {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
return idbRequest(store.getAll());
}
// Recommended: idb library (by Jake Archibald)
// import { openDB } from 'idb';
// Provides a cleaner API
Cookie Attributes
Cookies are small pieces of data that a server stores on the client. They are automatically sent to the server with every HTTP request.
// Basic cookie setting
document.cookie = 'username=Alice';
// Including multiple attributes
document.cookie = 'sessionId=abc123; Path=/; Secure; SameSite=Strict; Max-Age=3600';
// Cookie attribute descriptions:
// Path : path to which the cookie is sent (default: current path)
// Domain : domain to which the cookie is sent (default: current domain)
// Max-Age : seconds until expiry (Max-Age=0 → delete immediately)
// Expires : expiry date/time (Max-Age takes precedence)
// Secure : only sent over HTTPS
// HttpOnly : not accessible via JavaScript (can only be set by server)
// SameSite : CSRF protection (Strict, Lax, None)
// Read cookies (returns all cookies as a string)
console.log(document.cookie);
// 'username=Alice; theme=dark; language=en'
// Cookie parsing helper
function parseCookies() {
return Object.fromEntries(
document.cookie
.split('; ')
.filter(Boolean)
.map(cookie => {
const [key, ...rest] = cookie.split('=');
return [key.trim(), decodeURIComponent(rest.join('='))];
})
);
}
function getCookie(name) {
const cookies = parseCookies();
return cookies[name] ?? null;
}
// Cookie setting helper
function setCookie(name, value, options = {}) {
const {
maxAge,
expires,
path = '/',
domain,
secure = true,
sameSite = 'Lax'
} = options;
let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (maxAge !== undefined) cookieStr += `; Max-Age=${maxAge}`;
if (expires) cookieStr += `; Expires=${expires instanceof Date ? expires.toUTCString() : expires}`;
if (path) cookieStr += `; Path=${path}`;
if (domain) cookieStr += `; Domain=${domain}`;
if (secure) cookieStr += '; Secure';
cookieStr += `; SameSite=${sameSite}`;
document.cookie = cookieStr;
}
// Delete a cookie
function deleteCookie(name, path = '/') {
document.cookie = `${name}=; Max-Age=0; Path=${path}`;
}
// Usage
setCookie('theme', 'dark', { maxAge: 365 * 24 * 60 * 60 }); // 1 year
getCookie('theme'); // 'dark'
deleteCookie('theme');
Detailed Explanation of the SameSite Attribute
// SameSite=Strict: only sent for same-site requests
// - Cookie not sent even when clicking a link from an external site
// - Strongest CSRF protection
// - Suitable for authentication cookies (prevents session hijacking)
// SameSite=Lax (default): same site + GET navigation from external sites
// - Can be sent when navigating via external link clicks
// - POST requests, etc. are blocked
// - A balanced choice for most situations
// SameSite=None: sent for all requests
// - Secure attribute is required
// - Necessary for cross-site authentication (OAuth, payments, etc.)
// - CSRF risk → recommended to use alongside CSRF tokens
setCookie('session', token, {
sameSite: 'Strict',
secure: true,
httpOnly: true // can only be set server-side
});
HttpOnly and Secure Attributes
// HttpOnly: not accessible via JavaScript
// - Cannot be read via document.cookie
// - Protects cookies from XSS attacks
// - Can only be set server-side (Set-Cookie header)
// Server response header:
// Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
// Secure: only sent over HTTPS
// - Cookie not included when sent over HTTP
// - Prevents man-in-the-middle attacks
// - Works without this in development (localhost)
// HttpOnly cookies are invisible from the client
console.log(document.cookie); // HttpOnly cookies not shown here
Storage Limits and Security Considerations
Capacity Limits
// Check storage usage (Storage API)
const estimate = await navigator.storage.estimate();
console.log(`Used: ${(estimate.usage / 1024 / 1024).toFixed(2)}MB`);
console.log(`Quota: ${(estimate.quota / 1024 / 1024).toFixed(2)}MB`);
// Request persistent storage
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
console.log(isPersisted ? 'Persistent storage granted' : 'Using temporary storage');
}
}
// Handle QuotaExceededError
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
if (e.name === 'QuotaExceededError') {
// Delete old entries or notify the user
clearOldEntries();
try {
localStorage.setItem(key, value);
} catch {
console.error('Save failed: storage is full');
}
}
}
}
Security Considerations
// 1. Do not store sensitive information
// Things that should NOT be stored in localStorage/sessionStorage:
// - Passwords
// - Credit card information
// - Personally identifiable information (SSN, etc.)
// - Private API keys
// Bad examples
localStorage.setItem('password', 'user_password'); // absolutely forbidden!
localStorage.setItem('creditCard', '1234-5678-...'); // absolutely forbidden!
// JWT token storage debate:
// localStorage: vulnerable to XSS, safe from CSRF
// HttpOnly Cookie: safe from XSS, vulnerable to CSRF (mitigated with SameSite)
// Recommended: HttpOnly Cookie + SameSite=Strict/Lax + CSRF token
// 2. XSS prevention
// Escaping is required when inserting localStorage data into the DOM
const stored = localStorage.getItem('userContent');
element.textContent = stored; // safe (not parsed as HTML)
// element.innerHTML = stored; // dangerous! XSS possible
// 3. Same-origin policy
// localStorage is isolated per origin (protocol + host + port)
// https://example.com vs http://example.com → different storage
// https://example.com vs https://sub.example.com → different storage
// 4. Third-party access
// If an iframe or another script has the same origin, it can access storage
// → Be careful about loading untrusted scripts
// 5. Privacy mode
// Some browsers restrict storage access in privacy mode or delete it when the session ends
function isStorageAvailable(type) {
let storage;
try {
storage = window[type];
const testKey = '__storage_test__';
storage.setItem(testKey, testKey);
storage.removeItem(testKey);
return true;
} catch (e) {
return e instanceof DOMException && (
e.code === 22 ||
e.code === 1014 ||
e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
) && storage?.length !== 0;
}
}
Real-World Example: State Persistence
// Auto-save form data (draft saving)
class FormAutoSave {
#form;
#storageKey;
#saveTimer;
#debounceDelay;
constructor(form, { storageKey, debounceDelay = 500 } = {}) {
this.#form = form;
this.#storageKey = storageKey ?? `form-autosave-${form.id}`;
this.#debounceDelay = debounceDelay;
this.#restore();
this.#bindEvents();
}
#save() {
clearTimeout(this.#saveTimer);
this.#saveTimer = setTimeout(() => {
const data = Object.fromEntries(new FormData(this.#form));
sessionStorage.setItem(this.#storageKey, JSON.stringify(data));
}, this.#debounceDelay);
}
#restore() {
const saved = sessionStorage.getItem(this.#storageKey);
if (!saved) return;
const data = JSON.parse(saved);
Object.entries(data).forEach(([name, value]) => {
const field = this.#form.elements[name];
if (field) field.value = value;
});
}
#bindEvents() {
this.#form.addEventListener('input', () => this.#save());
this.#form.addEventListener('submit', () => {
sessionStorage.removeItem(this.#storageKey);
});
}
clear() {
sessionStorage.removeItem(this.#storageKey);
}
}
// Usage
const form = document.querySelector('#post-form');
const autoSave = new FormAutoSave(form, { storageKey: 'new-post-draft' });
// Persisting user settings
class UserSettings {
static #key = 'user-settings';
static #defaults = {
theme: 'system',
language: 'en',
fontSize: 16,
notifications: true,
compactMode: false
};
static get() {
try {
const stored = localStorage.getItem(this.#key);
return stored
? { ...this.#defaults, ...JSON.parse(stored) }
: { ...this.#defaults };
} catch {
return { ...this.#defaults };
}
}
static set(updates) {
const current = this.get();
const updated = { ...current, ...updates };
localStorage.setItem(this.#key, JSON.stringify(updated));
window.dispatchEvent(new CustomEvent('settings:changed', { detail: updated }));
return updated;
}
static reset() {
localStorage.removeItem(this.#key);
window.dispatchEvent(new CustomEvent('settings:changed', { detail: this.#defaults }));
}
}
// Usage
UserSettings.set({ theme: 'dark' });
const { theme, language } = UserSettings.get();
Expert Tips
1. Storage Abstraction Layer
// Change storage strategy depending on the environment
class Storage {
static create(strategy = 'local') {
const strategies = {
local: new LocalStorageStrategy(),
session: new SessionStorageStrategy(),
memory: new MemoryStorageStrategy(), // for SSR/testing
indexedDB: new IndexedDBStrategy() // for large data
};
return strategies[strategy];
}
}
2. Storage Encryption (for non-sensitive data only)
// AES-GCM encryption with SubtleCrypto
async function encryptData(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(JSON.stringify(data));
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
};
}
3. Syncing State Between Tabs with Storage Events
// Change in one tab → detected in another tab (localStorage only)
window.addEventListener('storage', (e) => {
if (e.key === 'cart') {
const cart = JSON.parse(e.newValue ?? '[]');
updateCartUI(cart);
}
if (e.key === 'logout-signal') {
// Log out all tabs
window.location.href = '/login';
}
});
// Send logout signal
function logoutAllTabs() {
localStorage.setItem('logout-signal', Date.now().toString());
localStorage.removeItem('logout-signal');
}