Without notes, state yesterday’s main idea and one unresolved question.
Local storage and JSON
JavaScript Foundations II and the Browser
Objective
Persist simple browser data and understand serialization limits.
Events are interrupts, promises represent future results, and the DOM is the browser’s live model of the page.
- JSON stringify and parse
- localStorage keys
- defensive loading
Why this matters
Your inventory app is honest and useless in exactly one way: press refresh and everything is gone. The array lives in memory, and memory belongs to the page, which the browser throws away on reload.
Today you make it survive. You will convert your array into text, store that text in the browser, read it back on the next visit, and — the part most tutorials skip — handle the case where what comes back is missing or damaged. By the end the app remembers your components across refreshes and across closing the browser entirely.
JSON: structure as text
Storage and networks move text and bytes, not JavaScript objects. To save a nested array of objects you must first flatten it into a string, and later rebuild it. That conversion is serialization, and the standard text format for it is JSON — JavaScript Object Notation, the format Day 1 mentioned as what servers send back.
JSON looks like JavaScript object syntax with stricter rules:
[{ "id": "R1", "name": "Resistor 10k", "quantity": 42, "inStock": true }]
- Property names are always in double quotes. Single quotes are invalid.
- Strings use double quotes. No trailing commas. No comments.
- Allowed values: string, number, boolean,
null, array, object. That is the whole list.
Two built-in functions do the conversion, and they are exact opposites:
const text = JSON.stringify(components); // array -> string
const back = JSON.parse(text); // string -> array
JSON.stringify takes an optional third argument for indentation, which is what makes a file
readable by a human:
console.log(JSON.stringify({ id: "C1", quantity: 8 }, null, 2));
{
"id": "C1",
"quantity": 8
}
You cannot put a struct on the wire
A serial link carries a byte stream. To send a struct you pack it into bytes at one end and
unpack it at the other, and both ends must agree on the layout. Anything that is not data —
function pointers, addresses that only mean something in this process — cannot survive the trip.
JSON.stringify is the pack step, JSON.parse is the unpack, and the "allowed values" list above
is the agreed layout.
What survives the round trip
Serialization is lossy, and knowing what it drops prevents a whole class of confusing bugs.
const record = {
id: "C1",
quantity: 8,
inStock: true,
discontinued: null,
addedAt: new Date("2026-08-03T09:00:00Z"),
supplier: undefined,
reorder() { return "ordering"; },
};
console.log(JSON.stringify(record));
{"id":"C1","quantity":8,"inStock":true,"discontinued":null,"addedAt":"2026-08-03T09:00:00.000Z"}
Three things happened. supplier and reorder vanished — undefined values and functions are
not JSON, so those keys are silently dropped. null survived, because null is a JSON value.
And addedAt, a Date object, became a string. Parse it back and typeof parsed.addedAt is
"string", not an object with .getFullYear() on it. Nothing warns you; the crash arrives later.
The rule: store plain data. If you need a date back as a Date, rebuild it yourself with
new Date(parsed.addedAt) after parsing.
localStorage: keys and strings
localStorage is a small key–value store built into every browser. It survives reloads, tab
closes, and browser restarts, and it has three methods you need:
localStorage.setItem("inventory:components", text);
const raw = localStorage.getItem("inventory:components");
localStorage.removeItem("inventory:components");
Four facts that decide how you use it:
- Everything is a string.
localStorage.setItem("count", 42)stores the text"42", andgetItemgives you"42"back. This is exactly whyJSON.stringifycomes first: an array is not a string, so the browser converts it with the default rules and you get useless text like[object Object],[object Object]. getItemreturnsnullwhen the key does not exist. First visit, cleared storage, renamed key — all producenull, andJSON.parse(null)does not do what you want.- Storage is per origin. Everything served from
http://localhost:3000shares one store, so every project you serve on that port sees the same keys. Namespace them:inventory:components, notdata. - It is roughly 5 MB and completely visible. The user can read and edit every value in DevTools.
localStorage is not a safe place for secrets
Anything stored here is plain text, readable and editable by the user and by any script running on the page. Never put passwords, API keys, or personal data in it. It is for convenience state — a draft, a preference, a local inventory — and nothing that matters if it is tampered with. Day 51 covers where credentials actually belong.
Look at the store yourself
On your served page, open DevTools → Application → Local Storage → your localhost
origin. It is a table of keys and values, and you can edit a value in place. Run
localStorage.setItem("test", "hello") in the Console and watch the row appear.
Defensive loading
Saving is easy. Loading is where apps break, because the stored value can be in three bad states: missing, unparseable, or parseable but the wrong shape. All three happen in real use — a first visit, a value someone edited by hand, or data written by an older version of your own code.
JSON.parse on invalid text throws, which on Day 24 you learned stops the program:
JSON.parse("not json");
SyntaxError: Unexpected token 'o', "not json" is not valid JSON
To survive that, wrap it in try/catch. The try block runs the risky code; if anything inside
it throws, execution jumps straight to catch with the error object, and the program continues
instead of stopping.
const STORAGE_KEY = "inventory:components:v1";
function isValidComponent(item) {
return (
item !== null &&
typeof item === "object" &&
typeof item.id === "string" &&
typeof item.name === "string" &&
Number.isInteger(item.quantity)
);
}
function loadComponents() {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw === null) return []; // first visit: not an error
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return []; // right text, wrong shape
return parsed.filter(isValidComponent); // drop damaged records
} catch (error) {
console.warn("Stored inventory was unreadable; starting empty.", error);
return [];
}
}
Every branch returns a usable array, so the rest of the app never has to ask whether loading
worked. That is the whole point of defensive loading: absorb the mess at the boundary and hand
clean data inward. The :v1 in the key is cheap insurance — change your object shape later and you
can move to :v2 without tripping over old records.
Reading someone else's handwriting
A form arrives from outside your office. It might be blank, it might be soaked and unreadable, or it might be filled in on last year's form with fields you no longer use. A clerk who assumes every form is perfect jams the whole queue on the first bad one. A clerk who checks each form and sets aside the unusable ones keeps working. Stored data is a form from outside — even when you wrote it yourself last week.
Walkthrough
Add persistence to yesterday's app. Put STORAGE_KEY, isValidComponent, and loadComponents
above your other code, then add saving and start-up loading:
function saveComponents() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(components));
}
Call it wherever state changes — the two functions that already exist for exactly that reason:
function addComponent(item) {
components.push(item);
saveComponents();
render();
}
function deleteComponent(id) {
components = components.filter((item) => item.id !== id);
saveComponents();
render();
}
And replace the initial let components = [] and the bare render() at the bottom:
let components = loadComponents();
render();
Serve the page, add two components, and press refresh. They are still there. Open Application →
Local Storage and you will see one row: the key inventory:components:v1 and a value that is
your array as JSON text.
Now break it on purpose. In the Console:
localStorage.setItem("inventory:components:v1", "not json");
location.reload();
The page loads with an empty list and a warning in the Console — no crash, no blank screen. That is
catch doing its job. Now try the subtler damage: set the value to '{"a":1}' and reload. It
parses fine but is not an array, so Array.isArray sends you to an empty list.
Finally, a reset the user can reach:
document.querySelector("#reset-button").addEventListener("click", () => {
if (!confirm("Delete all components? This cannot be undone.")) return;
localStorage.removeItem(STORAGE_KEY);
components = [];
render();
});
confirm shows a browser dialog and returns true or false. Removing the key, rather than
storing "[]", leaves storage genuinely clean.
Checkpoint
Add a component, close the tab completely, reopen the served address. It is still there. Then
explain why JSON.stringify is needed before setItem — in one sentence, about strings.
Your turn
Build the deliverable: inventory persists across refresh and can reset safely.
- Add
loadComponents,saveComponents, and theSTORAGE_KEYto your app. Add three components and refresh. All three survive. - Inspect the stored value in Application → Local Storage. Confirm it is one JSON string containing every field you expect, with double-quoted property names.
- Add the
#reset-buttonand its listener. Reset, refresh, and confirm the list is still empty and the key is gone from the storage table. - Corrupt the value by hand in the DevTools table — delete a closing brace — and reload. Record the console warning. The app must still open.
- Store one object with a
quantityof"12"(a string, not a number) by editing storage directly, then reload.isValidComponentshould drop it. Confirm it does not appear. - Add an
addedAt: new Date().toISOString()field to new components. Confirm after a reload that it comes back as a string, and format it for display withnew Date(item.addedAt).
You are done when
Components survive a refresh and a browser restart, reset clears them permanently, and the app still opens cleanly when the stored value is missing, corrupt, or the wrong shape.
Pair mode — after your loader handles the three bad cases
Today's mode is pair: take one small task at a time, and before accepting an AI-generated change, inspect the diff, run your checks, and understand the behaviour it produces.
"Review my storage loading code for parse failures and invalid shapes."
Test every case it raises by writing that bad value into storage yourself and reloading. A suggestion you have not reproduced is not yet a finding.
Common pitfalls
- Storing the array without
stringify.setItem(key, components)stores text like[object Object],[object Object]. Refresh and everything is gone, with no error to explain it. JSON.parse(localStorage.getItem(key))with no null check. On a first visit that isJSON.parse(null), which returnsnullrather than throwing — so yourcomponentsbecomesnulland the next.filterfails with aTypeError.- Saving in
render. It is tempting and it is wrong:renderis for drawing, and drawing should never write. Save where the state changes. - Expecting
Dateobjects back. They return as strings. Rebuild withnew Date(...)if you need date methods.
Verify it yourself
Open today's reference, MDN's Dynamic scripting with JavaScript, and find its pages on JSON and on client-side storage.
- MDN documents
sessionStoragenext tolocalStorage. Find it and write one sentence on how its lifetime differs, and one situation where you would prefer it. - Find MDN's note on the storage quota. What error does exceeding it produce, and what does that mean for an app that saves on every keystroke?
Add both answers to notes/day-27.md. Tomorrow you assemble the whole week, and knowing the limits
of your storage is part of knowing when your app would fail.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Save inventory data, reload the page, and recover it. Handle missing or malformed data.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
Inventory persists across refresh and can reset safely.
Working with AI today
Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.
Review my storage loading code for parse failures and invalid shapes.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.