3.8 Saving and Loading Data
SLO 2
Describe the principles of structured programming.
A structure in memory and a line of text are not the same thing, and this is where that boundary gets a defined crossing: stringify on the way out, parse on the way back, with the shape preserved rather than flattened into text nothing can rebuild.
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Loading data you did not write is the case where a program meets input it cannot trust, so the accepted method is to parse inside try...catch and carry on. Saving by key is what makes a result outlive the run that produced it.
Learning Objectives
By the end of this section you should be able to:
- Explain why an object cannot simply be written out and read back as text.
- Convert an object or array to a JSON string with
JSON.stringify. - Convert a JSON string back into a usable value with
JSON.parse. - Handle a parse failure with
try...catchinstead of letting it stop the program. - Save and load values by key so they survive the program ending.
- Say why this book never calls
open(),read()orwrite().
3.8.1 Data Has to Outlive the Program
Everything you have built so far exists only while the program runs. Close the tab and the objects, arrays and variables are gone. That is fine for a calculation and useless for a high score, a saved game, or a document.
To keep data, you have to get it out of the program and store it somewhere — and everything that stores data stores text. Files hold text. Browser storage holds text. Networks send text. So the question becomes: how do you turn an object into text and back again without losing anything?
The naive attempt does not work:
▶ Press Run to see the output…
What you should see:
[object Object] string
You do get a string. It just contains nothing — the name, the age and the courses are all gone, replaced by a label saying "this was an object". There is no way back from that.
What you need is a text format that keeps the structure. That format is JSON.
JSON (JavaScript Object Notation) is a text format for representing objects, arrays, numbers, strings, booleans and null. It looks almost exactly like a JavaScript literal, which is where it came from, and nearly every programming language can read and write it.
[object Object] appearing on a page or in a log is one of the most recognizable symptoms in JavaScript, and it always means the same thing: something turned an object into text the lazy way. The moment you see it, you are looking for the place a value should have been converted properly and was not.
3.8.2 JSON.stringify: Object to Text
JSON.stringify converts a value into a JSON string:
▶ Press Run to see the output…
What you should see:
{"name":"Marisol","age":19,"courses":["CSCI 4","MATH 105"]}
string
Nothing was lost. Every property is there, the nested array survived, and the whole thing is now a single string you can store or send.
Look closely at what changed from the JavaScript literal:
- Every key is in double quotes —
"name", notname. JSON requires this; JavaScript object literals do not. - Strings use double quotes only. Single quotes are not valid JSON.
- There are no spaces or line breaks, because none are needed.
JSON.stringify()JSON.stringify(value) converts a JavaScript value into a JSON-formatted string. Objects, arrays, numbers, strings, booleans and null all convert. The result is a string, whatever the input was.
Readable output
A second and third argument make the output human-readable, which is what you want when a person will look at it:
▶ Press Run to see the output…
What you should see:
{
"name": "Marisol",
"age": 19
}
The null is a filter you almost never need; the 2 is how many spaces to indent by. Use it for anything a human reads, and leave it off for anything only a program reads — the compact form is smaller.
What does not survive
JSON has no way to represent a function, so functions are dropped silently:
▶ Press Run to see the output…
What you should see:
{"count":5}
No error, no warning — the method is simply not there. undefined values disappear the same way.
That silence is worth expecting rather than discovering. JSON stores data, not behaviour. If you save an object and load it back and its methods have vanished, nothing went wrong — you saved the data, which is all JSON was ever able to carry. Section 3.5.5 called a method "a property whose value is a function"; this is the one place that distinction has a visible consequence.
1. What does JSON.stringify([1, 2, 3]) produce?
- The array
[1, 2, 3] - The string
"[1,2,3]" [object Object]
Solution
b. The string "[1,2,3]". Arrays convert too, not just objects, and the result is always a string.
Build an object describing a book with a title, a page count, and an array of two tags. Print it as compact JSON, then as indented JSON.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Output:
{"title":"Eloquent JavaScript","pages":472,"tags":["programming","javascript"]}
{
"title": "Eloquent JavaScript",
"pages": 472,
"tags": [
"programming",
"javascript"
]
}
3.8.3 JSON.parse: Text Back to Object
JSON.parse is the other direction — a JSON string becomes a real value you can use:
▶ Press Run to see the output…
What you should see:
Marisol 20 CSCI 4 2
That is the proof it worked: student.age + 1 gives 20, not "191". The age came back as a real number, the courses came back as a real array with a real length. This is an object like any other.
JSON.parse()JSON.parse(text) converts a JSON-formatted string back into a JavaScript value. Objects become objects, arrays become arrays, and numbers become numbers.
A round trip
Together the two make a round trip — out to text and back:
▶ Press Run to see the output…
What you should see:
{"width":10,"height":4,"tags":["red","small"]}
14
small
The round trip has a second use worth knowing. restored is a brand-new object — not another name for original, the way a plain assignment would give you (Section 3.6):
▶ Press Run to see the output…
What you should see:
999 10
Changing it through alias changed original, because they are the same object. copy was rebuilt from text and is completely independent, all the way down through the nested object. Section 3.7's { ...original } copies only the top level; this copies everything.
1. After const v = JSON.parse('{"n": 5}');, what is v.n + 1?
6"51"undefined
Solution
a. 6. JSON.parse restores the number as a number, so arithmetic works. Had n been the string "5" in the JSON, the answer would have been "51".
Take the JSON string below, parse it, and print the second item's price plus 10.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Output:
15 Pad
Reading data.items[1].price is the nesting from Section 3.5.4 — an array of objects inside an object.
3.8.4 Parsing Can Fail
JSON.stringify always works. JSON.parse does not, because the text handed to it might not be valid JSON — and text you load is text you did not write.
▶ Press Run to see the output…
What you should see:
Could not read the saved data. SyntaxError
JSON.parse throws on bad input. Without the try...catch from Section 2.5, that error would stop the program.
This is the realistic case, not a contrived one. Saved data gets truncated, edited by hand, written by an older version of your program, or simply is not there yet the first time someone runs it. Every JSON.parse of data you did not create in the same breath belongs inside a try...catch:
▶ Press Run to see the output…
What you should see:
{"theme":"dark","fontSize":18}
{"theme":"light","fontSize":14}
Good data is used; bad data falls back to sensible defaults. The program keeps running either way, which is the whole point of Section 2.5.
Note what the catch block does not do — it does not return undefined and hope for the best. A recovery that hands back a usable value is the difference between handling an error and merely surviving it. Section 2.5.2 warned about the empty catch; this is what a full one looks like.
1. Why does JSON.parse need a try...catch when JSON.stringify does not?
JSON.parseis slower.stringifystarts from a valid JavaScript value;parsestarts from text that may be malformed.stringifyreturnsnullon failure instead.
Solution
b. You always hand stringify a real value, so there is nothing to be invalid. parse is given text — often text from storage, a file, or a network — and text can be anything at all.
Write a function safeParse(text) that returns the parsed value, or the string "unreadable" when the text is not valid JSON. Test it both ways.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Output:
{"ok":true}
unreadable
3.8.5 Saving by Key
JSON gives you text. Storing that text so it survives the page closing is a separate job, and in a browser the simplest tool for it is localStorage.
It works like an object with three methods, and it holds strings only:
localStorage.setItem(key, text)— save under a name.localStorage.getItem(key)— read it back, ornullif nothing was saved.localStorage.removeItem(key)— delete it.
▶ Press Run to see the output…
What you should see:
1200 null
Two things to be careful about, and they are the two that catch everybody.
Everything comes back as a string. Save the number 1200 and you get the string "1200":
▶ Press Run to see the output…
What you should see:
12001 1201
The first line joined two strings instead of adding two numbers. Run it and watch it happen — this is the single most common storage bug there is.
Missing means null, not undefined. A key that was never saved gives null, so a first run has to cope with that:
▶ Press Run to see the output…
What you should see:
null First run — using defaults.
Key-value storage saves a piece of text under a name you choose and gives it back when you ask for that name. localStorage is the browser's version: the data survives the page closing, and everything stored is a string.
The whole pattern
Put the three pieces together — stringify to save, parse to load, try...catch because the load can fail — and you have the shape every save system uses:
▶ Press Run to see the output…
What you should see:
{"theme":"dark","fontSize":18}
{"theme":"light","fontSize":14}
Read loadSettings as three questions in order: is there anything saved?, can it be read?, what do we use if either answer is no? Every one of them has to be answered, and the defaults answer the last two.
This one genuinely persists. Run it, then reload the page and run only the loadSettings half — the value is still there, because localStorage survives the page closing. That is the difference between this and every other example in the book, all of which forget everything the moment they finish. You will use the same pattern in Section 6.5, where moSHion wraps it as storeItem and getItem to save a game.
1. After localStorage.setItem("count", 5), what does localStorage.getItem("count") + 1 give?
6"51"null
Solution
b. "51". Storage holds strings, so the 5 was converted to "5" on the way in and + joined it to "1". Number(localStorage.getItem("count")) + 1 gives 6.
2. What does localStorage.getItem("neverSaved") return?
undefinednull- An empty string
Solution
b. null. That is why a load function tests text === null before trying to parse — JSON.parse(null) does not throw, it quietly gives back null, and the missing-data case would slip through unnoticed.
3.8.6 Why There Is No open() Here
Other languages teach saving data with file functions: open a file by name, read it, write to it, close it. You may have seen open(), read() and write() in a course that used one of those languages.
JavaScript running in a browser has none of them, and the reason is deliberate. A web page runs code from a stranger's server on your computer. If that code could open any file it liked, visiting a page would mean handing over your documents. So the browser gives a page no access to your filesystem at all. It gets storage of its own — like localStorage — kept separate for each site.
Two doors exist, both requiring a person to open them:
- A file the user picks through a file-chooser dialog. The page never sees anything the user did not choose.
- A file the page offers as a download, which the user then chooses to save.
Neither appears in this book, because nothing in chapters 4 to 13 needs one. The JSCAD chapters export finished models through JSCAD's own export system, and the moSHion chapters save games to key-value storage. When you meet those two doors in other JavaScript, you will now know why they are shaped the way they are: not as an inconvenience, but as the reason it is safe to visit a web page at all.
1. Why can a web page not open a file on your computer by name?
- JavaScript is too slow to read files.
- Any page could then read your files just because you visited it.
- Files must be converted to JSON first.
Solution
b. A page runs code you did not write, so it is given no filesystem access at all. Reading a file requires the user to choose it.
Problem Set 3.8
3.8.1 What does JSON.stringify([1, 2, 3]) produce, and what type is the result?
Solution
Step 1 — Run the conversion: JSON.stringify accepts any JSON-compatible value, and arrays are one of them.
Step 2 — Check the type: The result is always a string, whatever the input was. typeof on the result gives "string".
Answer: It produces the string "[1,2,3]", whose type is string. Arrays convert just like objects do.
3.8.2 After const v = JSON.parse('{"n": 5}');, what is v.n + 1, and why is it not "51"?
Solution
Step 1 — Parse the string: JSON.parse('{"n": 5}') builds a real object { n: 5 }, where n is the number 5.
Step 2 — Do the arithmetic: Since v.n is a number, v.n + 1 is numeric addition:
Step 3 — Why not "51": String concatenation would only happen if n had come back as the string "5" (e.g. if the JSON contained "n": "5"). Parsing restores numbers as numbers, so + adds instead of joining.
Answer: v.n + 1 is 6. It is not "51" because JSON.parse converts the number in the JSON into an actual JavaScript number, so + performs addition rather than concatenation.
3.8.3 Why does JSON.parse need a try...catch when JSON.stringify does not?
Solution
Step 1 — Consider what each function receives: JSON.stringify always starts from a real JavaScript value you already have in memory, so there is nothing about it to be invalid. (It is not literally incapable of failing — an object that contains itself makes it throw — but that is a shape you built yourself, not text handed to you from outside.)
Step 2 — Consider what parse receives: JSON.parse starts from text, often text loaded from storage, a file, or a network that you did not write at the same moment. That text can be truncated, hand-edited, written by an older version of your program, or simply garbage.
Step 3 — What happens on bad input: JSON.parse throws a SyntaxError when the text is not valid JSON. Without a try...catch, that error stops the program; with one, you can fall back to defaults and keep running.
Answer: Because stringify starts from a valid JavaScript value while parse starts from text that may be malformed — and malformed text makes parse throw, so the call must be wrapped in try...catch to recover gracefully.
3.8.4 After localStorage.setItem("count", 5), what does localStorage.getItem("count") + 1 give, and how do you fix it?
Solution
Step 1 — Recall what storage holds: localStorage holds strings only. Saving the number 5 converts it to the string "5" on the way in.
Step 2 — Evaluate the expression: So localStorage.getItem("count") returns "5", and:
The + joins two strings instead of adding two numbers.
Step 3 — Fix it: Convert the retrieved value back to a number before doing arithmetic:
Number(localStorage.getItem("count")) + 1 // 6
Answer: It gives the string "51". Fix it by wrapping the read in Number(...) (or parseInt) so the addition happens numerically: Number(localStorage.getItem("count")) + 1 gives 6.
3.8.5 What does localStorage.getItem("neverSaved") return, and why does a load function have to test for it?
Solution
Step 1 — Identify the return value: A key that was never saved gives back null — not undefined, and not an empty string.
Step 2 — Why a load function must test for it: On a first run, nothing has been saved yet, so every read returns null. If the load function passes that straight to JSON.parse, note that JSON.parse(null) does not throw — it quietly returns null — so the missing-data case would slip through unnoticed and downstream code would crash trying to use null as an object.
Step 3 — The correct pattern: Test explicitly before parsing:
const text = localStorage.getItem("settings");
if (text === null) {
return defaults;
}
Answer: It returns null. A load function must test for null first because a missing key means "nothing saved yet," and skipping the test lets null flow silently through parsing into code that expects a real object.
3.8.6 Why can a web page not open a file on your computer by name?
Solution
Step 1 — Recall the sandbox rule: A web page runs JavaScript written by someone else — a stranger's server sends code to your computer every time you visit. If that code could open any file by name, merely visiting a page would expose your documents to whoever wrote the page.
Step 2 — What the browser allows instead: The browser gives a page no filesystem access at all. It gets its own isolated storage (like localStorage, kept separate per site), plus two doors that require a person's action: a file the user picks through a file-chooser dialog, or a download the user chooses to save.
Answer: Because any page could then read your files just because you visited it. The browser deliberately provides no filesystem access so that visiting a page is safe; reading a local file requires the user to choose it.
3.8.7 Explain what [object Object] means when you see it, and what should have been written instead.
Solution
Step 1 — Explain what it means: [object Object] is the default string form of any object in JavaScript. Seeing it means somewhere an object was turned into text the lazy way — e.g. by concatenating it ("" + obj) or interpolating it into a template string — instead of being converted properly.
Step 2 — Why it signals data loss: The label carries none of the object's properties. There is no way back from [object Object]; the name, values, and nested structure are all gone.
Step 3 — What should have been written instead: Use JSON.stringify(obj) to produce a faithful text version that keeps every property and can be restored later with JSON.parse.
Answer: [object Object] means an object was converted to text without JSON.stringify, destroying all its data. The fix is JSON.stringify(obj), which preserves the structure as readable, restorable JSON.
3.8.8 Build an object with a title, a number, and an array of two tags. Print it as compact JSON and as indented JSON.
Solution
Step 1 — Build the object: Create an object with a title (string), a number, and an array of two tags.
const book = {
title: "Eloquent JavaScript",
pages: 472,
tags: ["programming", "javascript"]
};
Step 2 — Print compact JSON: Call stringify with no extra arguments — compact output is smaller and meant for programs.
console.log(JSON.stringify(book));
{"title":"Eloquent JavaScript","pages":472,"tags":["programming","javascript"]}
Step 3 — Print indented JSON: Pass null (the unused filter) and 2 (indent width) for human-readable output.
console.log(JSON.stringify(book, null, 2));
{
"title": "Eloquent JavaScript",
"pages": 472,
"tags": [
"programming",
"javascript"
]
}
Answer: Compact: {"title":"Eloquent JavaScript","pages":472,"tags":["programming","javascript"]}. Indented: the same object printed across multiple lines with two-space indentation via JSON.stringify(book, null, 2).
3.8.9 Parse the string below and print the second item's price plus 10.
▶ Press Run to see the output…
Solution
Step 1 — Parse the string: Convert the JSON text into a usable object.
const data = JSON.parse(json);
Step 2 — Navigate the nesting: data.items is an array of objects inside the object (Section 3.5.4 nesting). The second item is index 1, so its price is data.items[1].price.
Step 3 — Add 10 and print: The parsed price is a real number, so addition works directly.
console.log(data.items[1].price + 10); // 15 console.log(data.items[1].name); // Pad
Answer: data.items[1].price + 10 prints 15 (the second item, "Pad", has price 5).
3.8.10 Write safeParse(text) returning the parsed value or "unreadable" on bad input.
Solution
Step 1 — Wrap the parse in try...catch: Attempt the parse; if the text is invalid, JSON.parse throws a SyntaxError which the catch block intercepts.
function safeParse(text) {
try {
return JSON.parse(text);
} catch (err) {
return "unreadable";
}
}
Step 2 — Test both paths: Valid JSON parses normally; garbage triggers the catch and returns the fallback string.
console.log(safeParse('{"ok":true}')); // {"ok":true}
console.log(safeParse("{oops")); // unreadable
Answer:
function safeParse(text) {
try {
return JSON.parse(text);
} catch (err) {
return "unreadable";
}
}
It returns the parsed value for valid JSON and the string "unreadable" otherwise.
3.8.11 What happens to a method when its object is passed through JSON.stringify, and why is that not a bug?
Solution
Step 1 — State what happens: The method is dropped silently. JSON has no way to represent a function, so JSON.stringify simply omits it — no error, no warning.
const counter = { count: 5, describe: function () { return "..."; } };
JSON.stringify(counter); // {"count":5}
Step 2 — Explain why it is not a bug: JSON stores data, not behaviour. A method is a property whose value is a function, and functions are exactly the thing JSON was never able to carry. When you save an object and load it back without its methods, nothing went wrong — you saved the data, which is all JSON can represent. (In practice, methods are reattached by defining them on the object's shape after loading.)
Answer: The method vanishes from the JSON output, dropped silently along with undefined values. This is not a bug because JSON represents data only — functions are behaviour, and behaviour is expected to be rebuilt by the program after loading.
3.8.12 Explain how JSON.parse(JSON.stringify(obj)) differs from { ...obj } when obj has an object nested inside it.
Solution
Step 1 — Recall what spread copies: { ...obj } creates a new object but copies only the top-level properties. A property holding a nested object still points at the same inner object shared with the original.
Step 2 — Trace the difference: With obj = { size: { width: 10 } }, changing shallowCopy.size.width also changes obj.size.width, because both .size properties reference one object.
Step 3 — What the round trip does: JSON.parse(JSON.stringify(obj)) serializes everything to text and rebuilds it from scratch, so even the nested object is brand-new and completely independent — a deep copy.
const original = { size: { width: 10, height: 4 } };
const copy = JSON.parse(JSON.stringify(original));
copy.size.width = 999;
console.log(original.size.width); // 10 — unaffected
Answer: { ...obj } is a shallow copy: the nested object is shared with the original, so changes through either name affect both. The stringify/parse round trip rebuilds every level from text, producing a fully independent deep copy.
3.8.13 Write saveSettings and loadSettings for a settings object, where loadSettings returns defaults if nothing is stored or the stored text is corrupt.
Solution
Step 1 — Write saveSettings: Stringify the settings object and store it under a key. Storage holds strings, so the object must be converted to JSON text first.
function saveSettings(settings) {
localStorage.setItem("settings", JSON.stringify(settings));
}
Step 2 — Write loadSettings with three checks: First, is anything stored? (getItem returns null if not.) Second, can it be read? (try...catch around the parse.) Third, what to use if either answer is no? (the defaults.)
const defaults = { theme: "light", fontSize: 14 };
function loadSettings() {
const text = localStorage.getItem("settings");
if (text === null) {
return defaults;
}
try {
return JSON.parse(text);
} catch (err) {
return defaults;
}
}
Step 3 — Verify both failure paths: No key saved → returns defaults; corrupt text like "{ not json" throws inside the try → catch returns defaults. Good text → parsed object comes back.
Answer:
function saveSettings(settings) {
localStorage.setItem("settings", JSON.stringify(settings));
}
function loadSettings() {
const text = localStorage.getItem("settings");
if (text === null) return defaults;
try {
return JSON.parse(text);
} catch (err) {
return defaults;
}
}
It handles all three cases: nothing stored, corrupt text, and valid data.
3.8.14 Give one kind of data worth saving in a game and one that is not worth saving, and justify each in a sentence.
Solution
Step 1 — Pick data worth saving: A player's progress — current level, score, or position. Losing it would mean the player restarts from scratch every time they close the page, which defeats the purpose of playing across sessions.
Step 2 — Pick data not worth saving: Transient state such as the positions of enemies mid-animation, particles currently on screen, or the contents of a menu that is open right now. This state is regenerated fresh each run and saving it could even restore the game into an inconsistent state.
Answer: Worth saving: the player's high score or level progress, because it must survive between sessions to have meaning. Not worth saving: transient per-frame state like particle positions, because it is recreated every run and persisting it adds complexity with no benefit.
Key Terms
JSON -- JavaScript Object Notation, a text format for objects, arrays, numbers, strings, booleans and null, readable by nearly every language.
JSON.stringify() -- Converts a JavaScript value into a JSON string. Functions and undefined values are dropped silently.
JSON.parse() -- Converts a JSON string back into a JavaScript value. Throws a SyntaxError on malformed text.
Round trip -- Converting a value to JSON and back; the result is a fully independent copy, nested values included.
[object Object] -- What you get when an object is turned into text without JSON.stringify. All the data is lost.
Key-value storage -- Saving text under a chosen name and reading it back by that name; localStorage is the browser's.
localStorage -- Browser storage that survives the page closing. Holds strings only, and returns null for a key never saved.
Sandbox -- The rule that a web page gets no access to your filesystem, which is why JavaScript in a browser has no open().