localStorage & sessionStorage
Learn the browser Storage API: saving strings with localStorage and sessionStorage, storing JSON safely, reacting to storage events, and knowing when not to use it.
- 01Save and read small valuesUse
setItem,getItem,removeItem,key(index), andlength, and delete only your own keys. - 02Save objects safelyTurn objects into JSON text and back, handle broken saved text, and update old saved data to a new shape.
- 03Pick the right storageExplain the
storageevent, space limits, why the page waits for storage, why secrets don't belong there, and when to use IndexedDB or a cookie.
Two places to save text in the browser
The browser can save small pieces of text for a website, so they are still there later. This is called Web Storage. It comes in two parts:
localStoragekeeps text for a long time. It is still there after you reload the page, close the browser, and come back next week. It goes away only when your code or the user deletes it.sessionStoragekeeps text for one browser tab. A reload keeps it. Closing the tab deletes it.
Both save text in pairs. The key is a name, like theme. The value is the text saved under that name, like dark.
Each website gets its own storage. More exactly, each origin does. An origin is the start of a web address. It has three parts:
- the scheme, like
https; - the host, like
example.com; - the port, a number that is often hidden, like
443.
https://example.com and http://example.com are two different origins. They can't read each other's storage.
const theme = localStorage.getItem("theme") ?? "light";
localStorage.setItem("theme", "dark");
console.log(theme);
console.log(localStorage.getItem("theme"));Line 1 reads the saved theme with getItem. Nothing is saved yet, so getItem gives null, which means "nothing here". The ?? operator then picks a fallback: a backup value, here "light".
Line 2 saves "dark" with setItem. Line 3 prints light. Line 4 reads the theme again and prints dark. On a real site, the next visit would start with dark.
sessionStorage.setItem("step", 2);
console.log(sessionStorage.getItem("step"));
console.log(typeof sessionStorage.getItem("step"));Line 1 saves the number 2 in this tab's sessionStorage. Line 2 prints 2. Line 3 prints string: the number came back as text. The next section explains why.
Here is what happens to this tab's sessionStorage:
- Reload the tab: the value stays.
- Close the tab: the value is deleted.
- Open the same page in a new tab: the new tab starts empty.
- Duplicate the tab: in most browsers, the new tab starts with a copy. After that, the two copies change separately.
localStorage keeps the theme after a reload and after the browser restarts. sessionStorage keeps the step after a reload, but loses it when the tab closes.Imagine each website has its own small office. The office has a drawer for things that must still be there next week. Each open tab also has a sticky note on its own screen. A second tab gets its own sticky note, even though it works in the same office.
- In real life: A drawer in this website's office
- In JavaScript:
localStorage: text this origin keeps for a long time - In real life: Only this office's staff can open the drawer
- In JavaScript: Only pages from the same origin can read those keys
- In real life: A sticky note on one tab's screen
- In JavaScript:
sessionStorage: text for one tab that stays after a reload - In real life: The note goes in the bin when the tab closes
- In JavaScript: Closing the tab deletes its
sessionStorage
Use Web Storage for small things that only the browser needs, such as:
- a setting, like the theme;
- which tab or panel was open;
- whether the user has already seen a feature tour;
- a short draft of a form.
Don't use it as a database, as a safe place for secrets, or as a way to send data to the server. The section on limits explains why.
Save, read, and remove values
INTERACTIVElocalStorage and sessionStorage have the same methods. A method is a function you call on an object, after a dot:
setItem(key, value)saves a value under a key.getItem(key)reads the value saved under a key.removeItem(key)deletes one key and its value.lengthtells you how many keys are saved. It is a number, not a method, so it has no brackets.key(index)gives the name of the key at a position: 0, 1, 2, and so on.clear()deletes every key for this origin. Use it with care: it also deletes keys that other parts of the site saved.
localStorage.theme = "dark" also seems to save a value. Use setItem("theme", "dark") instead. It is clear, it matches getItem, and it can't clash with a name the storage object already has, such as length.
const storage = localStorage; storage.setItem("count", 7);storage.setItem("darkMode", false);storage.setItem("profile", { name: "Ada" }); console.log(storage.getItem("count"));console.log(storage.getItem("darkMode"));console.log(storage.getItem("profile"));console.log(storage.getItem("missing"));No cf-demo keys are saved right now.
Click a button to save demo keys for this site only.
One rule matters most: storage keeps only strings. A string is text. Before saving, the browser turns every value into text with String(value):
- The number
7becomes"7". - The boolean
falsebecomes"false". - An object like
{ name: "Ada" }becomes"[object Object]". The data inside is lost.
If a key was never saved, getItem gives null. That is why you often see a fallback after ??, like localStorage.getItem("tab") ?? "overview".
localStorage.setItem("count", 5);
const savedCount = localStorage.getItem("count");
console.log(savedCount);
console.log(typeof savedCount);Line 1 saves the number 5. Storage turns it into the text "5". Line 3 prints 5, and line 4 prints string. Before you do math with it, turn it back into a number with Number(savedCount).
setItem gets the number 5, but storage keeps the text "5". So getItem gives back a string.localStorage.setItem("notice", "saved");
console.log(localStorage.getItem("notice"));
localStorage.removeItem("notice");
console.log(localStorage.getItem("notice"));Line 2 prints saved. Line 3 deletes the key. Line 4 reads the same key again and prints null. It is not an empty string, and it is not undefined.
localStorage.setItem("app:theme", "dark");
localStorage.setItem("app:draft", "Tea");
localStorage.setItem("other:setting", "keep");
localStorage.removeItem("app:theme");
console.log(localStorage.length);
localStorage.clear();
console.log(localStorage.length);Here is what each line does:
- Lines 1 to 3 save three keys. Two start with
app:. One starts withother:. - Line 4 deletes only
app:theme. Line 5 prints2. - Line 6 calls
clear(). It deletesapp:draft, and it also deletesother:setting, which belonged to another part of the site. - Line 7 prints
0.
So give your keys a shared start, called a prefix, like app:. Then delete only the keys that start with it.
localStorage.setItem("theme", "dark");
localStorage.setItem("draft", "Tea");
for (let index = 0; index < localStorage.length; index += 1) {
console.log(localStorage.key(index));
}The loop starts at position 0. Before each turn, it checks index < localStorage.length. On each turn, key(index) gives one key name. This prints theme, then draft.
Here the keys come out in the order they were saved. Browsers don't promise that order, so don't write code that depends on it.
Think of storage as a locker that only fits written notes. To keep an object in it, first write the object down as a note. Later, read the note and build the object again.
- In real life: A written note fits in the locker
- In JavaScript: A string, like
dark, fits in storage - In real life: Write a big object down as a note first
- In JavaScript:
JSON.stringify(settings)turns the object into a string - In real life: Read the note to build the object again
- In JavaScript:
JSON.parse(saved)turns the string back into an object
Save objects as JSON text
INTERACTIVETo save an object, first turn it into text. JSON is a text format for data. Two tools do the work:
JSON.stringify(value)turns an object into JSON text.JSON.parse(text)turns JSON text back into an object.
Saved text can go wrong. It can be:
- missing, because nothing was saved yet;
- old, saved by an older version of your app;
- broken, for example after someone changed it by hand in the browser's developer tools.
JSON.parse throws an error when the text is broken. So read saved JSON inside try/catch. The try block runs the code. If it throws, the catch block runs instead, and the page doesn't crash.
function readJSON(key, fallback) {
const text = localStorage.getItem(key);
if (text === null) return fallback;
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(readJSON("missing", { theme: "light" }).theme);
localStorage.setItem("settings", "{broken json");
console.log(readJSON("settings", { theme: "light" }).theme);Here is what happens:
- Line 11 reads the key
"missing".getItemgivesnull, so line 3 returns the fallback right away. It printslight. - Line 12 saves broken text under the key
"settings". - Line 13 reads it.
JSON.parseon line 5 throws, so thecatchon lines 6 and 7 returns the fallback. It printslightagain, and the page keeps working.
function readJSON(key, fallback) { const text = localStorage.getItem(key); if (text === null) return fallback; try { return JSON.parse(text); } catch { return fallback; }} const oldSettings = { version: 1, data: { theme: "dark", compact: true }};localStorage.setItem("cf-demo:settings", JSON.stringify(oldSettings)); const loaded = readJSON("cf-demo:settings", { version: 2, data: {} });const current = loaded.version === 1 ? { version: 2, data: { ...loaded.data, textSize: "normal" } } : loaded; localStorage.setItem("cf-demo:settings", JSON.stringify(current));{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}string{"version":2,"data":{"theme":"dark","compact":true,"textSize":"normal"}}The version 1 settings were read safely, changed into the version 2 shape, and saved back as JSON text.
const draft = { title: "Tea", body: "Buy leaves" };
localStorage.setItem("draft", JSON.stringify(draft));
const restored = JSON.parse(localStorage.getItem("draft"));
console.log(restored.title);Line 1 makes a small draft object. Line 2 turns it into JSON text and saves it. Line 3 reads the text and turns it back into an object. Line 4 prints Tea.
A form could do this while the user types. In a real app, read with the safe reader above, in case the saved text is old or broken.
JSON.stringify turns the whole draft object into one string, and storage saves it. JSON.parse turns the string back into an object, so restored.title is "Tea".Once real users have saved data, a later version of your app may need it in a new shape. So save a version number with the data, like { version: 2, data: settings }. When you read version 1 data, change it into the version 2 shape first. This is called a migration. Then the rest of your app only ever sees the newest shape.
The table shows four kinds of saved value. Read each row as: what you have, what storage holds, and what to do about it.
| What you have | What storage holds | What to do |
|---|---|---|
| A plain object | JSON text, like {"theme":"dark"}. | Use JSON.stringify before saving and JSON.parse after reading. |
| A missing key | Nothing. getItem gives null. | Return a fallback. Don't pass null to JSON.parse by mistake. |
| Broken text | Text like {broken. | Catch the error from JSON.parse and return a fallback. |
A Date object | A string, like 2026-09-25T00:00:00.000Z. | JSON gives back only the string. Call new Date(text) if you need date methods again. |
The storage event
INTERACTIVEAn event is a message the browser sends when something happens. When one page changes localStorage, the browser sends a storage event to the other open pages from the same origin. These can be other tabs, or an iframe: a page shown inside another page.
A change to sessionStorage sends the event too, but only to pages that share that tab's storage, such as an iframe in the same tab.
The page that made the change does not get the event. It already knows what it changed. So use the event to hear about changes from other pages, and update your own page directly.
"hello" with setItem. Only the other tab, Tab B, gets a storage event; Tab A does not hear its own change.You change something in the office drawer and shout down the hallway, "I changed the drawer!" People in other rooms hear it and can act on it. You don't need the shout, because you made the change yourself.
- In real life: You change something in the drawer
- In JavaScript: One tab calls
localStorage.setItem - In real life: People in other rooms hear your shout
- In JavaScript: Other tabs and iframes from the same origin get a
storageevent - In real life: You don't need to hear your own shout
- In JavaScript: The page that saved gets no event
- In real life: The shout says what changed
- In JavaScript: The event has
key,oldValue,newValue,url, andstorageArea
window.addEventListener("storage", (event) => { console.log(event.key); console.log(event.oldValue); console.log(event.newValue); console.log(event.url); console.log(event.storageArea === localStorage);}); localStorage.setItem("cf-demo:signal", "hello");The iframe is another page from the same origin, so it reports each change. This page saved the value, so it gets no event: its own event count is 0.
The event tells you what changed:
key: the key that changed.oldValueandnewValue: the text before and after the change.url: the address of the page that made the change.storageArea: which storage changed,localStorageorsessionStorage.
If a page calls clear(), event.key is null. This lesson's demos never call localStorage.clear(). They delete only keys that start with cf-demo:, because this website saves its own keys too, like your theme.
Example: remember the last tab
STEP THROUGHMany sites remember the last tab, panel, or filter you picked. The code is short. It has four steps:
- Read the saved tab, with a fallback.
- The visitor picks a tab.
- Save the new tab name.
- On the next visit, read it again.
The step-through below uses a small pretend storage, built on a Map. It has the same getItem and setItem methods. The lesson uses it because Node.js, which runs this lesson's checks, has no browser localStorage.
Step through four moves: read a saved value with a fallback, let the visitor pick, save the new value, then read it back on a later visit.
script
const fallback = "overview";let activeTab = storage.getItem("lastTab") ?? fallback; activeTab = "examples";storage.setItem("lastTab", activeTab); const nextVisit = storage.getItem("lastTab") ?? fallback;console.log(nextVisit);The page changes the tab on screen first. Then it saves the choice. Storage doesn't control every click. It only helps choose the first tab on the next visit.
Limits, and when not to use it
KNOW THE LIMITSWeb Storage is small on purpose. Many browsers allow about 5 MB for each origin, but the real limit changes from browser to browser. A quota is the most space you are allowed.
When storage is full, setItem throws an error named QuotaExceededError. Catch that error, so the page keeps working without the saved value.
const storage = {
setItem() {
const error = new Error("storage is full");
error.name = "QuotaExceededError";
throw error;
}
};
try {
storage.setItem("draft", "Tea");
} catch (error) {
console.log(error.name);
console.log("Keep the draft in memory");
}Lines 1 to 7 make a pretend storage. Its setItem always throws an error with the same name a full browser storage uses. Line 10 tries to save. The catch block runs instead: line 12 prints QuotaExceededError, and line 13 prints Keep the draft in memory. The draft stays in a normal variable, so the form still works.
try { localStorage.setItem("cf-demo:settings", jsonText);} catch (error) { // Storage is full or not available: keep going without it.}Press the button to save one demo value, then delete it right away.
Press the button to save one demo value, then delete it right away.
Each row below is a case where Web Storage is the wrong choice. Read it as: the problem, why it matters, and what to use instead.
| The problem | Why it matters | What to use instead |
|---|---|---|
| Secrets, like login tokens | Any script on the page can read Web Storage. If an attacker gets their script onto your page (an XSS attack), it can steal them. | Keep secrets out of it. A cookie that the server sets with the HttpOnly flag is often safer, because page scripts can't read it. |
| Large or complex data | Everything is text. Each read and write also makes the page wait until it is done. | IndexedDB, a database in the browser for records, files, and lots of data. |
| The server needs the value | Web Storage is never sent to the server with requests. | A cookie, for small values the server needs. |
| Code in a worker | A worker is a script that runs in the background. localStorage and sessionStorage don't exist there. | IndexedDB, or send messages between the worker and the page. |
| Private browsing | Some browsers delete the data sooner, or don't keep it at all. | Check that storage works, catch errors, and treat saved values as optional. |
- The page waits: each read and write stops the page until it is done. Keep them few and small. Don't call storage in animations or in code that runs many times a second.
- Space can run out: about 5 MB is common, but the browser, the device, and privacy settings can change it.
- Everything comes back as text: parse JSON, and turn saved numbers and booleans back on purpose.
- Bigger data goes in IndexedDB: it stores real records, and the page doesn't wait for it.
- A duplicated tab gets a copy: both tabs start with the same
sessionStorage, but later changes in one don't reach the other. - Scripts can read it: an XSS attack can read storage, so tokens and passwords don't belong there.
Never save passwords, login tokens, card details, or other secrets in localStorage or sessionStorage. If an attacker's script runs on your page, it can read them.
localStorage, sessionStorage, IndexedDB, or a cookie?
SORT ITThe next lesson is about IndexedDB, a real database inside the browser. For now, use this simple rule:
- Small text that only the browser needs: Web Storage.
- A tiny value the server needs: a cookie. The browser sends cookies to the server with each request.
- Lots of data, or data with many fields: IndexedDB.
- Remember the site's light or dark theme after the browser restarts
- Keep a checkout form's current step for this tab only
- Store hundreds of notes for offline use, each with several fields
- Send a tiny language setting to the server with the next request
- Save a login token that would be dangerous if a page script could read it
- Remember the choices in a product filter panel on this site
- Keep a different preview mode in each of two tabs of the same site
- Keep large files or images for offline use
Put each case under the storage you would try first.
Common misconceptions
- "It stores JavaScript values." It stores only strings. Use JSON for plain objects. Rebuild special values, like a
Date, yourself after reading. - "sessionStorage is deleted when I reload." A reload keeps it. Closing the tab deletes it.
- "Every tab shares sessionStorage." Tabs from the same origin share
localStorage. Each tab has its ownsessionStorage. - "The storage event tells my page about its own change." The event goes to the other pages from the same origin, not to the page that saved.
- "It is safe because users can't see it." Users can see it in the developer tools, and any script on the page can read it. Never put secrets there.
- "clear() is a harmless reset." It deletes every key in that storage for this origin. Delete only your own keys, the ones with your prefix.
Practice exercises
5 EXERCISESThis code copies what storage does, using a Map. Predict the two lines it prints.
const storage = new Map();
storage.set("count", String(3));
storage.set("flag", String(false));
console.log(storage.get("count") + 2);
console.log(typeof storage.get("flag"));3 is saved as the text "3", so "3" + 2 joins them and prints 32. false is saved as the text "false", so typeof prints string.
Read the readJSON helper. Predict the two themes it prints.
function readJSON(text, fallback) {
if (text === null) return fallback;
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(readJSON('{"theme":"dark"}', {}).theme);
console.log(readJSON('{broken', { theme: "light" }).theme);function readJSON(text, fallback) {
if (text === null) return fallback;
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(readJSON('{"theme":"dark"}', {}).theme);
console.log(readJSON('{broken', { theme: "light" }).theme);The first text is good JSON, so it prints dark. The second text is broken, so JSON.parse throws. The catch returns the fallback, and its theme is light.
A developer saves a setting. Then they wait for a storage listener on the same page to update the screen. Will that listener run on the page that saved?
No. The page that calls setItem doesn't get its own storage event. Update that page directly, and use storage to hear about changes from other tabs or iframes.
This code copies storage with a Map. Predict what the last line prints.
const storage = new Map();
const fallback = "home";
let tab = storage.get("tab") ?? fallback;
tab = "settings";
storage.set("tab", tab);
console.log(storage.get("tab") ?? fallback);The first read finds nothing, so tab starts as home. Then the user picks settings, and the code saves it. The last read finds it and prints settings.
You are building a note-taking app. Decide where to keep each of these, and say why:
- the theme the user picked;
- a preview of unsaved changes in the current tab;
- hundreds of notes for offline use;
- the secret that keeps the user logged in.
One good plan: the theme goes in localStorage, because it should last. The preview for the current tab goes in sessionStorage, because it belongs to one tab. The hundreds of offline notes go in IndexedDB, because they are big. The login secret goes in an HttpOnly cookie that the server sets, so page scripts can't read it. Choose by how long the data lasts, how big it is, whether the server needs it, and whether it is secret.
Check your understanding
7 QUESTIONSQuestion 1 of 7This code copies
getItemwith aMap. What does it print for a key that was never saved?Read the code, then predictconst storage = new Map(); const getItem = (key) => storage.has(key) ? storage.get(key) : null; console.log(getItem("missing"));Choose an answer to see the explanation.
Question 2 of 7Storage turns every value into a string, like
String(42). What does this print?Read the code, then predictconst value = String(42); console.log(typeof value); console.log(value + 1);Choose an answer to see the explanation.
Question 3 of 7You want to save an object and get it back later. What should you pass to
setItem?Choose an answer to see the explanation.
Question 4 of 7One page changes
localStorage. Where does thestorageevent go?Choose an answer to see the explanation.
Question 5 of 7Which sentence about
sessionStorageis true?Choose an answer to see the explanation.
Question 6 of 7Storage may be full. What should your code do around
setItem?Choose an answer to see the explanation.
Question 7 of 7Which storage is best for lots of records with many fields that you need offline?
Choose an answer to see the explanation.
Key takeaways
localStoragekeeps text for an origin for a long time.sessionStoragekeeps text for one tab.- Storage keeps only strings.
getItemgivesnullfor a missing key. - Save objects as JSON. Read them with a fallback and
try/catch, and save a version number with them. - The
storageevent goes to the other pages from the same origin, not to the page that saved. - Catch errors when storage is full, never save secrets, and use IndexedDB or cookies when they fit better.
Final definition: Web Storage lets a page save small pieces of text in the browser, as key and value pairs, for one origin. localStorage keeps them across visits, and sessionStorage keeps them for one tab. Each read and write makes the page wait until it is done.
Up next: IndexedDB.