Snappy ToolsThree browser storage mechanisms, each with different lifetimes, scopes, and appropriate uses. Here's...
Three browser storage mechanisms, each with different lifetimes, scopes, and appropriate uses. Here's when to reach for each.
| Feature | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Capacity | ~5–10 MB | ~5 MB | ~4 KB |
| Expires | Never (until cleared) | Tab/session close | Set explicitly |
| Sent with requests | No | No | Yes (automatically) |
| Accessible from JS | Yes | Yes | Yes (unless HttpOnly) |
| Scope | Domain | Tab + domain | Domain (configurable) |
Stores data with no expiry. Persists until the user clears it or the site clears it.
// Write
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
// Read
const theme = localStorage.getItem('theme'); // 'dark'
const user = JSON.parse(localStorage.getItem('user'));
// Delete
localStorage.removeItem('theme');
// Clear all
localStorage.clear();
// Check existence
if (localStorage.getItem('onboarded') === null) {
// first visit
}
All values are strings. Use JSON.stringify / JSON.parse for objects and arrays.
Use for: user preferences (theme, language, layout), non-sensitive app state, data that should survive browser restarts.
Don't use for: authentication tokens, sensitive data. localStorage is accessible from any JavaScript on the page — an XSS vulnerability can read everything.
Same API as localStorage, but scoped to the current tab and cleared when the tab closes.
sessionStorage.setItem('cart', JSON.stringify(cartItems));
const cart = JSON.parse(sessionStorage.getItem('cart'));
Each tab gets its own sessionStorage. Opening the same URL in two tabs gives each tab independent storage — data in one tab is not visible in the other.
Use for: form state across multiple steps (wizard forms), tab-specific UI state, temporary data that shouldn't persist across sessions.
Don't use for: data that should survive refreshes (it does survive refreshes, but NOT tab close), data shared across tabs.
Cookies are old but have specific advantages: they're automatically sent with every HTTP request to the domain, and can be marked HttpOnly to prevent JavaScript access.
// Set a cookie (JavaScript)
document.cookie = 'visited=true; max-age=86400; path=/';
// Read cookies (awkward — they're all in one string)
document.cookie // "visited=true; theme=dark; lang=en"
// Parsing requires manual work or a library:
function getCookie(name) {
return document.cookie.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1];
}
// Delete (set expired)
document.cookie = 'visited=; max-age=0; path=/';
Cookie attributes that matter:
Set-Cookie: token=abc123;
HttpOnly; ← JS cannot read this cookie
Secure; ← only sent over HTTPS
SameSite=Strict; ← not sent on cross-site requests
Max-Age=86400; ← expires in 24 hours
Path=/ ← sent for all paths
Use for: session tokens (always set HttpOnly + Secure + SameSite), any data the server needs with each request, cross-subdomain data sharing with Domain=.example.com.
Don't use for: large data (4KB limit), client-side-only preferences (unnecessary server overhead), anything that doesn't need to be sent to the server.
This is where developers make the most consequential storage decision.
JWTs in localStorage: convenient, but vulnerable to XSS. If any third-party script, browser extension, or injected code can run on your page, it can steal the token.
JWTs in HttpOnly cookies: the server sets the cookie with HttpOnly, JavaScript cannot read it, and XSS attacks can't steal it. The cookie is sent automatically with every request. This is the more secure pattern.
// Server sets the cookie (Node.js / Express example)
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
The tradeoff: HttpOnly cookies require CSRF protection (the SameSite attribute handles most cases). localStorage tokens are easier to implement but have a larger XSS attack surface.
For most applications: HttpOnly cookies for auth tokens, localStorage for non-sensitive preferences.
localStorage changes in one tab fire an event in other tabs from the same origin:
// In tab A, tab B will receive this event when tab A calls localStorage.setItem
window.addEventListener('storage', (event) => {
console.log(event.key); // 'theme'
console.log(event.newValue); // 'dark'
console.log(event.oldValue); // 'light'
});
This doesn't fire in the tab that made the change — only in other tabs. Useful for syncing state across tabs.
localStorage may be unavailable in private browsing on some browsers or when storage quota is exceeded:
function storageAvailable(type) {
try {
const storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch {
return false;
}
}
if (storageAvailable('localStorage')) {
localStorage.setItem('setting', 'value');
} else {
// fall back to a cookie or in-memory variable
}
Domain=.example.com
The key differences: localStorage outlives sessions and tabs; sessionStorage scopes to the current tab; cookies travel to the server and can be made inaccessible to JavaScript. Reach for the mechanism that matches the data's lifetime and who needs to read it.