localStorage, and why it is not a database
Everything you have built so far disappears on reload. localStorage is the
simplest fix — five methods, no setup — and it is also the most over-used tool in
front-end development. Both halves of that matter.
The whole API
localStorage.setItem('customer', 'Priya');
console.log(localStorage.getItem('customer'));
localStorage.removeItem('customer');
console.log(localStorage.getItem('customer'));
console.log(localStorage.length);
localStorage.clear();
Priya
null
0
That is all of it: setItem, getItem, removeItem, clear, length. It
survives reloads, tab closes and restarts, and it is per-origin — one site cannot
read another's.
A missing key gives null, not undefined and not an error.
Everything is a string
This is the rule the whole lesson hangs off.
localStorage.setItem('plates', 3);
console.log(typeof localStorage.getItem('plates'));
localStorage.setItem('order', { customer: 'Priya' });
console.log(localStorage.getItem('order'));
string
[object Object]
The number came back as '3'. The object was converted with String() and is
now the useless text [object Object] — no error, no warning, and the data is
gone.
Use JSON, from module 4:
const order = { customer: 'Priya', plates: 3 };
localStorage.setItem('order', JSON.stringify(order));
const back = JSON.parse(localStorage.getItem('order'));
console.log(back.plates + 1);
4
And everything module 4 said about JSON applies in full: dates come back as
strings, undefined properties vanish, Map and Set become {}. A Date
saved here and loaded back is text until you rebuild it with new Date(...).
One convenient accident: JSON.parse(null) returns null rather than throwing,
so reading a key that was never set is safe:
console.log(JSON.parse(localStorage.getItem('never-set')));
null
That is not true of JSON.parse(undefined), which throws — the difference
matters, and it is why getItem returning null is helpful here.
It can throw, and it can be empty
Two failures that only appear on someone else's device.
Storage can be full. The limit is around 5MB per origin. Exceeding it throws
a QuotaExceededError.
Storage can be unavailable. Some privacy modes and blocked-cookie settings make even reading throw.
So a wrapper is worth writing once:
export function save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
}
export function load(key, fallback = null) {
try {
const text = localStorage.getItem(key);
return text === null ? fallback : JSON.parse(text);
} catch {
return fallback;
}
}
That catch covers both the quota error and corrupted text — somebody's browser
extension, an interrupted write, or an older version of your own code that stored
a different shape. Never assume what you get back is what you wrote; it may
have been written by a version of your site from six months ago.
The page must still work when load returns the fallback. Storage is an
optimisation, not a foundation.
localStorage and sessionStorage
Identical APIs, different lifetimes:
localStorage |
sessionStorage |
|
|---|---|---|
| Survives reload | Yes | Yes |
| Survives tab close | Yes | No |
| Shared between tabs | Yes | No — per tab |
| Cleared by | Your code, or the user | Closing the tab |
sessionStorage is right for a multi-step form that should not reappear
tomorrow.
What it is not for
The important half of the lesson.
It is not a database. No querying, no indexes, no relationships. Everything is a string keyed by another string. Storing 500 orders and filtering them means loading and parsing all 500 every time.
It is not secure. Any script on your page can read all of it, and so can
anyone who opens devtools. Never put a password, a payment detail or an
authentication token in it. A single cross-site scripting hole — the
innerHTML risk from the selecting lesson — turns readable storage into a stolen
session. Tokens belong in httpOnly cookies, which JavaScript cannot read at
all.
It is not shared. It lives in one browser on one device. Your user's phone and laptop have different storage, and clearing site data wipes it. Anything that must follow a person belongs on a server.
It is synchronous, which means it blocks. Reading a large JSON blob on every keystroke will make a mid-range phone stutter. Read once into a variable, work with that, and write back when something changes.
So: use it for preferences, drafts, a cached list, "you were here last time". Not for anything you would be upset to lose, and not for anything secret.
When you need more — large data, structured queries, offline sync — the answer is IndexedDB or a real backend, and that is a different course.
A pattern that works
Keep state in a variable, persist as a side effect:
let orders = load('orders', []);
function addOrder(order) {
orders = [...orders, order];
save('orders', orders);
render();
}
The variable is the truth; storage is a copy. Reading from
localStorage every time you need the list would be slow and would scatter
parsing through your code. Module 4's non-mutating update, and the next lesson
builds on exactly this shape.
Check your work
getItem on a missing key returns null.
Everything is a string. setItem('plates', 3) comes back as '3', and
storing an object directly gives '[object Object]' with no error — the data is
lost.
JSON.stringify on the way in and JSON.parse on the way out, with all of
module 4's caveats: dates become strings, undefined properties vanish, Map
becomes {}.
JSON.parse(null) returns null rather than throwing, so parsing a
never-set key is safe. JSON.parse(undefined) does throw.
setItem can throw — QuotaExceededError at about 5MB, or a privacy setting
blocking access. Wrap reads and writes, and make the page work without them.
sessionStorage has the same API but dies with the tab and is not shared
between tabs.
It is not a database, not secure, not shared between devices, and synchronous. Never store tokens or passwords; any XSS hole reads all of it.
Keep state in a variable and treat storage as a copy.
Practice
- Store a name, read it back, remove it, and read it again to see the
null. - Store a number and prove it comes back as a string. Then add 1 to it and explain the result.
- Store an object directly with
setItemand look at what comes back. Confirm nothing errored. - Do it properly with
JSON.stringifyandJSON.parse, and do arithmetic on a number from the parsed object. - Store an object containing a
Date, read it back, and callgetFullYear()on it. Read the error, then rebuild the date. JSON.parsea key that was never set and confirm you getnullrather than an exception.- Write the
saveandloadwrapper. Testloadon a missing key and on a key whose value you have corrupted by hand in the Application panel. - Fill it up. Write a loop storing a large string repeatedly until it throws, and catch the error. Note which error you got and roughly how much fitted.
- Put something in
sessionStorageand something inlocalStorage. Reload — both survive. Close the tab and reopen — only one does. - Open the Application panel in devtools and look at your own stored data. Edit a value by hand and reload the page. That is what "not secure" means, seen with your own eyes.
- Harder. Take the order list from the delegation lesson and make it
survive a reload: state in a variable, saved on every change, loaded at
start with a fallback to an empty list. Then deliberately corrupt the stored
value in devtools and confirm the page still loads rather than showing a
blank screen. Finally, write a comment naming one thing in that list that
should not be in
localStorageif this were a real application.
Next: putting the whole module together — a to-do list with no framework.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship