JavaScript Local Storage
Local Storage is a browser API that lets JavaScript save key-value data directly in the user’s browser, with no expiration date and without sending that data to a server on every request. It is part of the Web Storage API (alongside sessionStorage) and is the standard way to persist small amounts of data — user preferences, cached UI state, draft form input — between page reloads and browser restarts. Because it lives entirely on the client, it is fast, synchronous, and available in virtually every modern browser.
Overview / How It Works
localStorage is exposed as a property of the global window object, so in browser code you can simply write localStorage instead of window.localStorage. It behaves like a simple dictionary that only stores strings: every key and every value is coerced to a string, even if you pass a number or boolean. If you try to store a plain object without converting it first, JavaScript calls the object’s toString() method behind the scenes, which produces the useless string "[object Object]". This is why real applications almost always pair localStorage with JSON.stringify() and JSON.parse() to serialize and deserialize structured data.
Data saved to localStorage is scoped to the page’s origin — the combination of protocol, domain, and port. A page at https://example.com cannot read data saved by https://sub.example.com or http://example.com (different protocol) or https://example.com:8080 (different port). Each origin gets its own isolated storage area, typically capped around 5–10 MB depending on the browser. Unlike cookies, nothing in localStorage is automatically sent with HTTP requests, which makes it both more efficient for large data and unsuitable as a replacement for authentication cookies that the server needs to see.
All localStorage methods are synchronous — they read from and write to disk on the main thread and block execution until they finish. For small amounts of data this is imperceptible, but writing large values repeatedly can cause jank in performance-sensitive code. Data written with localStorage persists indefinitely: it survives page reloads, tab closures, and full browser restarts, and is only removed when the user clears their browsing data, the site calls localStorage.clear(), or code explicitly removes a key. This is the key difference from sessionStorage, which shares the same API but is wiped when the tab closes.
| Storage type | Lifetime | Scope | Sent to server? |
|---|---|---|---|
localStorage |
Until explicitly cleared | Per origin, shared across tabs | No |
sessionStorage |
Until tab/window closes | Per origin, per tab | No |
| Cookies | Configurable expiry | Per domain (configurable path) | Yes, on every matching request |
Syntax
localStorage.setItem(key, value);
localStorage.getItem(key);
localStorage.removeItem(key);
localStorage.clear();
localStorage.key(index);
localStorage.length;
- setItem(key, value) — saves a value under a key. Both arguments are converted to strings.
- getItem(key) — returns the stored string, or
nullif the key does not exist. - removeItem(key) — deletes a single key-value pair. Does nothing if the key is absent.
- clear() — removes every key-value pair for the current origin.
- key(index) — returns the name of the key at a given position, useful for iterating.
- length — a read-only property giving the number of stored items.
Examples
Example 1: Basic set, get, and remove
localStorage.setItem('username', 'alice');
const username = localStorage.getItem('username');
console.log(username);
localStorage.setItem('theme', 'dark');
console.log(localStorage.length);
localStorage.removeItem('theme');
console.log(localStorage.getItem('theme'));
alice
2
null
The first call stores the string 'alice' under the key 'username', and getItem retrieves it unchanged. After adding a second key, length reports 2 items. Once 'theme' is removed, asking for it again returns null because getItem never throws for a missing key — it simply signals absence with null.
Example 2: Storing objects and arrays with JSON
const user = {
name: 'Bob',
age: 30,
roles: ['admin', 'editor']
};
localStorage.setItem('user', JSON.stringify(user));
const rawValue = localStorage.getItem('user');
const parsedUser = JSON.parse(rawValue);
console.log(parsedUser);
console.log(parsedUser.roles[0]);
localStorage.clear();
console.log(localStorage.getItem('user'));
{ name: 'Bob', age: 30, roles: [ 'admin', 'editor' ] }
admin
null
JSON.stringify() turns the user object into a JSON-formatted string that localStorage can actually store. JSON.parse() reverses that on the way out, reconstructing a real object with working array access. Calling clear() wipes every key for the origin, so the final getItem call correctly returns null.
Example 3: Feature detection and reusable helpers
function isLocalStorageAvailable() {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
}
function saveSettings(settings) {
localStorage.setItem('settings', JSON.stringify(settings));
}
function loadSettings() {
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : null;
}
if (isLocalStorageAvailable()) {
saveSettings({ fontSize: 16, darkMode: true });
console.log(loadSettings());
}
window.addEventListener('storage', (event) => {
console.log(`Key "${event.key}" changed from "${event.oldValue}" to "${event.newValue}"`);
});
{ fontSize: 16, darkMode: true }
Wrapping access in a try/catch lets you detect environments where storage is disabled (private browsing modes in some older browsers, or storage quota already exhausted) before relying on it. The storage event is special: it only fires in other tabs or windows on the same origin when a change happens, not in the tab that made the change — making it a lightweight way to sync UI state across open tabs.
How It Works Step by Step (Under the Hood)
- When you call
setItem, the browser converts both the key and value to strings, then writes the pair into an origin-scoped storage area that is typically backed by a file or embedded database (e.g. SQLite) on disk. - The write is synchronous from JavaScript’s perspective: the call blocks until the browser confirms the data is persisted, so the next line of code can safely assume the value was saved.
- Because storage is origin-scoped, the browser checks the current page’s protocol, host, and port before allowing access — there is no cross-origin read or write path through
localStorageitself. - Every write against an origin’s storage area is broadcast internally; any other browsing context (tab, window, or iframe) sharing that origin receives a
storageevent describing the key, old value, and new value. - If a write would exceed the browser’s storage quota for that origin, the engine throws a
DOMException(commonly namedQuotaExceededError) instead of silently failing or evicting old data. - Nothing in
localStorageis attached to outgoing network requests — retrieving it always requires explicit JavaScript running on a page from that origin.
Common Mistakes
Mistake 1: Forgetting to serialize objects
const preferences = { theme: 'dark', notifications: true };
localStorage.setItem('preferences', preferences);
console.log(localStorage.getItem('preferences'));
[object Object]
Passing an object directly to setItem silently converts it with toString(), destroying the actual data. Always call JSON.stringify() before saving an object or array, and JSON.parse() after reading it back, as shown in Example 2.
Mistake 2: Not handling quota errors
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (error) {
if (error instanceof DOMException && (error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED')) {
console.log('Storage quota exceeded. Consider clearing old data.');
} else {
console.log('Unexpected storage error:', error.message);
}
return false;
}
}
safeSetItem('bigData', 'x'.repeat(1000));
console.log(localStorage.getItem('bigData').length);
1000
Code that calls setItem without a try/catch will crash the whole script the moment a user’s storage fills up or the browser blocks storage entirely (e.g. some private-browsing modes throw on every write). Wrapping calls defensively, as safeSetItem does here, keeps the app functional even when storage is unavailable.
Mistake 3: Treating localStorage as secure or shared across origins
Anything stored in localStorage is readable by any JavaScript running on that page, including third-party scripts and, if the site has an XSS vulnerability, an attacker’s injected code. Never store passwords, raw authentication tokens, or other sensitive secrets there. It’s also easy to wrongly assume data set on www.example.com will be visible on example.com or api.example.com — it will not, since each is a distinct origin.
Best Practices
- Always wrap objects and arrays with
JSON.stringify()before storing andJSON.parse()after retrieving them. - Check that
getItem()did not returnnullbefore parsing, since parsingnullwith a fallback guard avoids runtime errors on first visit. - Namespace your keys (e.g.
myapp:settings) to avoid collisions with other scripts or future features on the same origin. - Never store sensitive data such as passwords, session tokens, or personal identifiers in
localStorage; it is fully readable by any script on the page. - Wrap reads and writes in try/catch to gracefully handle disabled storage or quota errors instead of letting them crash your app.
- Use the
storageevent to keep multiple open tabs in sync rather than polling storage on a timer. - Prefer
sessionStoragefor data that should only last for the current tab session, and reach forIndexedDBwhen you need to store large amounts of structured or binary data. - Periodically clean up keys you no longer need with
removeItem()rather than letting stale data accumulate indefinitely.
Practice Exercises
- Exercise 1: Write a function
toggleTheme()that reads a'theme'key fromlocalStorage(defaulting to'light'if unset), switches it between'light'and'dark', saves the new value, and returns it. - Exercise 2: Write a function
incrementVisitCount()that stores a number of page visits under the key'visits', parsing it withNumber(), incrementing it by 1 each call, saving it back as a string, and returning the new count. It should start at 1 if the key does not exist yet. - Exercise 3: Write a function
clearExpiredKeys(prefix)that loops over every key inlocalStorageusingkey(index)andlength, and removes any key that starts with the givenprefix. Hint: iterate backwards or collect matching keys into an array first, since removing items while iterating forward can skip entries.
Summary
localStoragestores string key-value pairs in the browser with no built-in expiration, scoped per origin.- Objects and arrays must be converted with
JSON.stringify()before saving andJSON.parse()after loading. - The core API is
setItem,getItem,removeItem,clear,key, and thelengthproperty. - All operations are synchronous and typically capped around 5–10 MB per origin, throwing a
QuotaExceededErrorwhen full. - The
storageevent fires in other tabs (not the originating one) whenever data changes, enabling cross-tab sync. - Never store sensitive data in
localStorage, since any script on the page can read it. - Use
sessionStoragefor per-tab data andIndexedDBfor larger or more structured storage needs.
