JavaScript localStorage: How to Store Data in the Browser
localStorage is a built-in browser API that saves key-value pairs for your site, and they stay after the tab or browser is closed. You store data with localStorage.setItem('key', 'value'), read it with localStorage.getItem('key'), and remove it with removeItem or clear(). It only stores strings, so objects and arrays go through JSON.stringify and JSON.parse. You get about 5 MiB per origin (MDN). Use it for small, non-sensitive data such as preferences, drafts and UI state.
The localStorage API in 5 methods
window.localStorage is a Storage object. The whole API:
// Save (overwrites any existing value for the key)
localStorage.setItem('theme', 'dark');
// Read: returns the string, or null if the key doesn't exist
const theme = localStorage.getItem('theme'); // "dark"
// Delete one key
localStorage.removeItem('theme');
// Delete everything this origin stored
localStorage.clear();
// Inspect keys
console.log(localStorage.length); // number of keys
console.log(localStorage.key(0)); // name of the first key (order isn't guaranteed)
Things to know from the start:
- Values are always strings.
setItem('count', 5)stores"5", andsetItem('user', {name: 'Ana'})stores the useless"[object Object]". getItemreturnsnullfor missing keys, notundefined. Use??for defaults.- Prefer the methods over property access (
localStorage.theme = 'dark'). Property access can collide with built-in names likelengthorkey. - It's per origin.
https://example.com,http://example.comandhttps://app.example.comeach have separate storage, and other sites can't read yours.
How to store objects and arrays in localStorage
Serialize with JSON on the way in, parse on the way out:
const settings = {theme: 'dark', fontSize: 16, tags: ['js', 'css']};
localStorage.setItem('settings', JSON.stringify(settings));
const saved = JSON.parse(localStorage.getItem('settings') ?? '{}');
console.log(saved.tags[0]); // "js"
Adding an item to a stored array means read, change, write back:
function addTodo(text) {
const todos = JSON.parse(localStorage.getItem('todos') ?? '[]');
todos.push({id: crypto.randomUUID(), text, done: false});
localStorage.setItem('todos', JSON.stringify(todos));
}
JSON drops what it can't represent: Date objects become strings, Map, Set, functions and undefined values are lost, and BigInt makes JSON.stringify throw. Store dates as ISO strings or timestamps and convert them back yourself.
A safe wrapper you can copy
Real code has to deal with three failures: the key is missing, the stored JSON is corrupt (someone edited it in DevTools, or an old app version wrote a different format), and writes that throw. Writes throw when the quota is full (QuotaExceededError), and even reading localStorage can throw a SecurityError when the user has blocked site data or the page runs from a file: or data: URL (MDN).
const storage = {
get(key, fallback = null) {
try {
const raw = localStorage.getItem(key);
return raw === null ? fallback : JSON.parse(raw);
} catch {
return fallback; // blocked storage or corrupt JSON
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.warn('localStorage is full; value not saved:', key);
}
return false;
}
},
remove(key) {
try { localStorage.removeItem(key); } catch {}
},
};
storage.set('draft', {title: 'Hello', body: '...'});
const draft = storage.get('draft', {title: '', body: ''});
localStorage with expiration
localStorage never expires on its own. If you want a TTL, store the expiry next to the value:
function setWithExpiry(key, value, ttlMs) {
localStorage.setItem(key, JSON.stringify({value, expires: Date.now() + ttlMs}));
}
function getWithExpiry(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const {value, expires} = JSON.parse(raw);
if (Date.now() > expires) {
localStorage.removeItem(key);
return null;
}
return value;
}
setWithExpiry('rates', {usd: 1, eur: 0.92}, 60 * 60 * 1000); // cache for 1 hour
Sync between tabs with the storage event
When one tab changes localStorage, the browser fires a storage event in other tabs and windows of the same origin, but not in the tab that made the change. That's an easy way to sync a logout or a theme change across tabs:
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
document.documentElement.dataset.theme = event.newValue ?? 'light';
}
if (event.key === null) {
// localStorage.clear() was called in another tab
}
});
For richer messaging between tabs, BroadcastChannel is the purpose-built API.
localStorage limits and gotchas
- Size: about 5 MiB each for localStorage and sessionStorage per origin, with a combined cap of 10 MiB, according to MDN. Keys and values are stored as UTF-16 strings, and exactly how browsers count them varies, so leave headroom.
- It's synchronous. Every call blocks the main thread. A few small reads are fine; parsing a multi-megabyte JSON blob on every keystroke causes jank.
- Not available in Web Workers or Service Workers. Use IndexedDB there.
- Private and incognito windows: storage works, but it's wiped when the last private tab closes.
- Safari can delete it. With Intelligent Tracking Prevention, Safari deletes all script-writable storage (localStorage included) for a site after seven days of Safari use with no user interaction on that site. Web apps added to the Home Screen are handled differently (WebKit). Treat localStorage as a cache, not as the only copy of anything important.
- Server-side rendering:
localStoragedoesn't exist in Node. In Next.js or other SSR code, only touch it in the browser (inuseEffect, or behindtypeof window !== 'undefined'), or you getReferenceError: localStorage is not defined.
Is localStorage secure? Don't store tokens in it
Any JavaScript running on your page can read localStorage. That includes your third-party scripts and any script injected through an XSS bug. OWASP's HTML5 Security Cheat Sheet advises against storing session identifiers or sensitive data there. In practice:
- Don't store passwords, credit card data or personal data.
- For auth sessions, prefer
HttpOnly,Secure,SameSitecookies, which JavaScript can't read. - If an SDK keeps tokens in localStorage (several auth SDKs do by default), keep them short-lived, set a strict Content Security Policy, and limit third-party scripts.
- Never
innerHTMLa value you read back from storage; a user can edit it in DevTools.
DevHunt lists security tools for scanning your front end for XSS and leaked secrets.
localStorage vs sessionStorage vs cookies vs IndexedDB
| localStorage | sessionStorage | Cookies | IndexedDB | |
|---|---|---|---|---|
| Lifetime | Until deleted | Until the tab closes | Until Expires/Max-Age |
Until deleted |
| Shared between tabs | Yes (same origin) | No, per tab | Yes | Yes |
| Size | ~5 MiB | ~5 MiB | ~4 KB per cookie | Large, a share of disk |
| Sent to server | No | No | Yes, with every request | No |
| API | Sync, strings only | Sync, strings only | document.cookie / Cookie Store API |
Async, structured data and blobs |
| Workers | No | No | Service workers, via Cookie Store API | Yes |
| Good for | Preferences, drafts, small caches | One-tab wizard or form state | Sessions (HttpOnly) | Offline data, files, large datasets |
sessionStorage has the same API (sessionStorage.setItem and so on). It survives a page reload but not closing the tab, and each tab gets its own copy. For anything large, structured or offline-first, use IndexedDB, usually through a small wrapper library such as idb or Dexie.
Inspect and edit localStorage in DevTools
In Chrome or Edge, open DevTools, go to Application > Storage > Local storage and select your origin to see, edit or delete keys. Firefox has the same under Storage, and Safari's Web Inspector under Storage. For more panels and shortcuts, see our Chrome DevTools guide.
When data needs to follow the user across devices, localStorage isn't enough; you need a backend. DevHunt's free Firebase alternatives list open-source and hosted options with free tiers.
FAQ
Where is localStorage data stored?
On the user's device, in the browser profile's data folder, separately for each origin. It isn't sent to your server and isn't synced between browsers or devices.
Does localStorage persist after closing the browser?
Yes. It stays until your code removes it, the user clears site data, or the browser evicts it (for example Safari's seven-day rule or storage pressure). Private windows are the exception: their data is deleted when the session ends.
How do I check if a key exists in localStorage?
Use localStorage.getItem('key') !== null. Don't test with if (localStorage.getItem('key')), because a stored empty string is falsy.
How do I get all items from localStorage?
Object.entries(localStorage) returns all key-value pairs as strings. Or loop with for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); ... }.
Can I use localStorage in React?
Yes. In a client-rendered app, read it in a lazy useState initializer (useState(() => storage.get('theme', 'light'))) and write it back in a useEffect when the value changes. With server rendering (Next.js and similar), start from a default and read localStorage in useEffect after mount, so the server and client HTML match.