Browser Storage
Browsers provide several mechanisms to store data on the client. Each has different lifetime, capacity, and security properties — choosing the wrong one leads to data loss, security vulnerabilities, or unnecessary network overhead.
Comparison
| Feature | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Lifetime | Until explicitly cleared | Tab/window session | Configurable expiry |
| Size limit | ~5–10 MB per origin | ~5–10 MB per origin | ~4 KB total |
| Sent to server | No | No | Yes (every HTTP request) |
| Accessible from JS | Yes | Yes | Yes (unless HttpOnly) |
| API complexity | Simple key-value | Simple key-value | Manual string parsing |
localStorage
Data persists across browser sessions until the user clears site data or your code removes it.
// Set values (always strings)
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ name: 'Alice', id: 42 }));
// Get values
let theme = localStorage.getItem('theme'); // 'dark'
let user = JSON.parse(localStorage.getItem('user')); // { name: 'Alice', id: 42 }
// Check existence
if (localStorage.getItem('token') === null) {
console.log('Not logged in');
}
// Remove specific key or all keys
localStorage.removeItem('theme');
localStorage.clear(); // removes ALL keys for this origin
// Iterate all keys
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
console.log(key, localStorage.getItem(key));
}
Storage Wrapper Pattern
Because localStorage only stores strings, wrap it for type safety:
const storage = {
get(key, defaultValue = null) {
const raw = localStorage.getItem(key);
if (raw === null) return defaultValue;
try { return JSON.parse(raw); }
catch { return raw; }
},
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key) {
localStorage.removeItem(key);
}
};
storage.set('preferences', { fontSize: 16, lang: 'en' });
storage.get('preferences'); // { fontSize: 16, lang: 'en' }
sessionStorage
Identical API to localStorage, but scoped to the current tab. Data is cleared when the tab closes.
// Save form draft so refresh doesn't lose work
sessionStorage.setItem('formDraft', JSON.stringify({
title: document.querySelector('#title').value,
body: document.querySelector('#body').value
}));
// Restore on page load
const draft = JSON.parse(sessionStorage.getItem('formDraft'));
if (draft) {
document.querySelector('#title').value = draft.title;
document.querySelector('#body').value = draft.body;
}
Use sessionStorage for: multi-step wizards, temporary form state, data that should not survive tab close.
Storage Events (Cross-Tab Sync)
The storage event fires in other tabs when localStorage changes — not in the tab that made the change:
window.addEventListener('storage', (e) => {
console.log('Key changed:', e.key);
console.log('Old value:', e.oldValue);
console.log('New value:', e.newValue);
if (e.key === 'theme') {
applyTheme(e.newValue);
}
});
Use this to sync theme or logout state across tabs.
Cookies
Cookies are small name-value pairs sent with every HTTP request to the matching domain. They are the traditional mechanism for session management.
// Set a cookie (client-side)
document.cookie = 'username=Alice; max-age=3600; path=/; SameSite=Strict; Secure';
// Read all cookies (returns one semicolon-separated string)
console.log(document.cookie); // 'username=Alice; session=abc123'
// Parse a specific cookie
function getCookie(name) {
return document.cookie
.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1];
}
console.log(getCookie('username')); // 'Alice'
Important Cookie Attributes
| Attribute | Purpose |
|---|---|
HttpOnly |
Not accessible via JavaScript (set server-side only) |
Secure |
Sent only over HTTPS |
SameSite=Strict |
Not sent on cross-site requests (CSRF protection) |
max-age / expires |
Control lifetime |
Modern authentication should use HttpOnly, Secure, SameSite cookies set by the server — never store JWTs in localStorage.
Practical Example: Theme Toggle
const THEME_KEY = 'app-theme';
function getTheme() {
return localStorage.getItem(THEME_KEY) || 'light';
}
function setTheme(theme) {
localStorage.setItem(THEME_KEY, theme);
document.documentElement.dataset.theme = theme;
}
// Apply saved theme on load
setTheme(getTheme());
document.querySelector('#toggle-theme').addEventListener('click', () => {
setTheme(getTheme() === 'light' ? 'dark' : 'light');
});
Security Best Practices
- Never store auth tokens in localStorage — any XSS attack can read them. Use HttpOnly cookies instead.
- Validate data read from storage — treat it as untrusted input; schema may have changed.
- Don’t store PII unnecessarily — minimize data at rest on the client.
- Use
SameSite=Stricton cookies to mitigate CSRF attacks. - Set short expiry on sensitive session cookies.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
QuotaExceededError |
Storage full (~5 MB) | Remove old keys; compress data; use IndexedDB for large data |
| Data lost after browser update | User cleared site data | Re-fetch from server; don’t rely solely on client storage |
storage event not firing |
Same tab made the change | Expected behavior — use CustomEvent for same-tab sync |
| Cookie not sent to API | Missing path=/ or wrong domain |
Set cookie attributes to match your API domain |
| JSON.parse throws | Corrupted or manually edited value | Wrap in try/catch; reset to default |
When to Use What
| Use Case | Recommended Storage |
|---|---|
| User theme / language preference | localStorage |
| Shopping cart (non-sensitive) | localStorage or sessionStorage |
| Multi-step form draft | sessionStorage |
| Authentication session | HttpOnly Secure cookie (server-set) |
| Large offline datasets | IndexedDB |
| Analytics tracking ID | Cookie (with consent) |
Browser storage is ideal for user preferences, UI state, and non-sensitive cached data — but server-side storage remains the source of truth for anything important.