Warm-up · Activity 1 of 7
Warm-up from lesson I6.1: the body of a POST request. What does this print?
const body = JSON.stringify({title: "Dune", year: 1965});
console.log(typeof body, body);// I6.4 · ~36 min · Intermediate
After this lesson you can keep settings and small lists in the browser with localStorage, store objects safely as JSON, and say when IndexedDB is the better choice.
You will be able to
Warm-up · Activity 1 of 7
const body = JSON.stringify({title: "Dune", year: 1965});
console.log(typeof body, body);Predict · Activity 2 of 7
localStorage.setItem("count", 3);
const count = localStorage.getItem("count");
console.log(count + 1);Practice · Activity 3 of 7
localStorage.____("theme", "dark");Practice · Activity 4 of 7
console.log(localStorage.getItem("nothing"), localStorage.length);Practice · Activity 5 of 7
localStorage.setItem("user", {name: "Ada"});
console.log(localStorage.getItem("user"));Brain teaser · Activity 6 of 7
const cart = JSON.parse(localStorage.getItem("cart")) ?? [];
cart.push("Dune");
localStorage.setItem("cart", JSON.stringify(cart));
console.log(localStorage.getItem("cart"), typeof localStorage.getItem("cart"));Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The page counts its visits and keeps a reading list in localStorage. main.js stores a number and reads it back as a string, saves an array as JSON and loads it again, and shows what happens to a value that was saved without JSON.stringify. At the end it clears the storage, so every run starts the same.
main.js
// Numbers come back as strings.
const visits = Number(localStorage.getItem("visits") ?? 0) + 1;
localStorage.setItem("visits", visits);
console.log("visits:", localStorage.getItem("visits"), typeof localStorage.getItem("visits"));
console.log("missing key:", localStorage.getItem("nothing"));
// Arrays and objects go through JSON.
function loadBooks() {
try {
return JSON.parse(localStorage.getItem("books")) ?? [];
} catch {
return []; // broken data: start with an empty list
}
}
function saveBooks(books) {
localStorage.setItem("books", JSON.stringify(books));
}
saveBooks([...loadBooks(), "Dune", "Emma"]);
console.log("stored:", localStorage.getItem("books"));
const books = loadBooks();
console.log("loaded:", books, books.length);
localStorage.setItem("books", "Dune,Emma"); // saved without JSON.stringify
console.log("broken:", loadBooks());
document.querySelector("#visits").textContent = "Visit number " + visits;
const list = document.querySelector("#books");
for (const title of books) {
const item = document.createElement("li");
item.textContent = title;
list.append(item);
}
localStorage.clear();
console.log("after clear:", localStorage.length);
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reading list</title>
</head>
<body>
<h1>My reading list</h1>
<p id="visits"></p>
<ul id="books"></ul>
<script type="module" src="main.js"></script>
</body>
</html>
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Output
visits: 1 string
missing key: null
stored: ["Dune","Emma"]
loaded: [ 'Dune', 'Emma' ] 2
broken: []
after clear: 0
<!-- #books -->
<ul id="books"><li>Dune</li><li>Emma</li></ul>Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Exercise 1 of 3
The page shows the theme and has a Light and a Dark button. On load it already shows the saved theme, or light. A click shows the chosen theme, but the starter never saves it, so after a reload the page is light again. Save the choice under the key "theme".
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run the tests: after a click, localStorage has no theme. Where does the page store anything?
The click listener knows the chosen theme.
localStorage.setItem("theme", theme); inside the click listener
One way to solve it. Yours can look different and still pass the checks.
const label = document.querySelector("#theme");
function show(theme) {
document.body.className = theme;
label.textContent = "Theme: " + theme;
}
show(localStorage.getItem("theme") ?? "light");
for (const theme of ["light", "dark"]) {
document.querySelector("#" + theme).addEventListener("click", () => {
show(theme);
localStorage.setItem("theme", theme);
});
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const label = document.querySelector("#theme");
function show(theme) {
document.body.className = theme;
label.textContent = "Theme: " + theme;
}
show(localStorage.getItem("theme") ?? "light");
for (const theme of ["light", "dark"]) {
document.querySelector("#" + theme).addEventListener("click", () => {
show(theme);
});
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const text = (page, selector) => page.document.querySelector(selector).textContent;
const click = (page, selector) => page.document.querySelector(selector).click();
test('Without a saved theme the page shows light', async () => {
const page = await loadPage();
assert.equal(text(page, '#theme'), 'Theme: light', `the page shows ${text(page, '#theme')}`);
});
test('A saved theme is shown on load', async () => {
const page = await loadPage({localStorage: {theme: 'dark'}});
assert.equal(text(page, '#theme'), 'Theme: dark', `the page shows ${text(page, '#theme')}`);
});
test('The chosen theme is still there after a reload', async () => {
const page = await loadPage();
click(page, '#dark');
assert.equal(page.window.localStorage.getItem('theme'), 'dark', `localStorage has theme ${page.window.localStorage.getItem('theme')}`);
const reloaded = await loadPage({localStorage: page.window.localStorage});
assert.equal(text(reloaded, '#theme'), 'Theme: dark', `after a reload the page shows ${text(reloaded, '#theme')}`);
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Theme</title>
</head>
<body>
<p id="theme"></p>
<button id="light" type="button">Light</button>
<button id="dark" type="button">Dark</button>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Run the checks (needs learnrun.js in the same folder):
npm install jsdom@28.1.0
node --testDownload learnrun.jsExercise 2 of 3
The page keeps a reading list under the key "books". It already loads the list safely and adds a title on each click of Add. But the starter saves the array itself, which localStorage turns into "Dune,Emma", so after a reload the list cannot be parsed and is empty. Save it as JSON.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run the tests: localStorage has books = Dune,Emma. What does setItem do with an array?
Anything that is not a string is turned into one the default way. JSON keeps the brackets and quotes.
localStorage.setItem("books", JSON.stringify(books));
One way to solve it. Yours can look different and still pass the checks.
const list = document.querySelector("#books");
const input = document.querySelector("#title");
function load() {
try {
return JSON.parse(localStorage.getItem("books")) ?? [];
} catch {
return []; // a broken value: start again
}
}
function render(books) {
list.replaceChildren(...books.map((title) => {
const item = document.createElement("li");
item.textContent = title;
return item;
}));
}
const books = load();
render(books);
document.querySelector("#add").addEventListener("click", () => {
books.push(input.value);
localStorage.setItem("books", JSON.stringify(books));
render(books);
input.value = "";
});
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const list = document.querySelector("#books");
const input = document.querySelector("#title");
function load() {
try {
return JSON.parse(localStorage.getItem("books")) ?? [];
} catch {
return []; // a broken value: start again
}
}
function render(books) {
list.replaceChildren(...books.map((title) => {
const item = document.createElement("li");
item.textContent = title;
return item;
}));
}
const books = load();
render(books);
document.querySelector("#add").addEventListener("click", () => {
books.push(input.value);
localStorage.setItem("books", books);
render(books);
input.value = "";
});
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const text = (page, selector) => page.document.querySelector(selector).textContent;
const click = (page, selector) => page.document.querySelector(selector).click();
const titles = (page) => [...page.document.querySelectorAll('#books li')].map((item) => item.textContent);
function add(page, title) {
page.document.querySelector('#title').value = title;
click(page, '#add');
}
test('Saved books are shown on load', async () => {
const page = await loadPage({localStorage: {books: '["Dune","Emma"]'}});
assert.deepEqual(titles(page), ['Dune', 'Emma'], `the list shows ${JSON.stringify(titles(page))}`);
});
test('A broken saved value gives an empty list', async () => {
const page = await loadPage({localStorage: {books: '{oops'}});
assert.deepEqual(titles(page), [], `the list shows ${JSON.stringify(titles(page))}`);
});
test('Added books are saved as JSON and survive a reload', async () => {
const page = await loadPage();
add(page, 'Dune');
add(page, 'Emma');
const saved = page.window.localStorage.getItem('books');
assert.equal(saved, '["Dune","Emma"]', `localStorage has books = ${saved}`);
const reloaded = await loadPage({localStorage: page.window.localStorage});
assert.deepEqual(titles(reloaded), ['Dune', 'Emma'], `after a reload the list shows ${JSON.stringify(titles(reloaded))}`);
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reading list</title>
</head>
<body>
<input id="title" aria-label="Title">
<button id="add" type="button">Add</button>
<ul id="books"></ul>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Run the checks (needs learnrun.js in the same folder):
npm install jsdom@28.1.0
node --testDownload learnrun.jsExercise 3 of 3
The page shows a count and a +1 button, and saves the count under the key "count". On a new page it counts 0, 1, 2. But with a saved count of 4, one click shows 41 instead of 5: the starter adds 1 to the string it read. Turn the saved value into a number when the page loads.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run the tests: a saved 4 becomes 41. What type does getItem give back?
"4" + 1 joins two strings. Turn the value into a number once, when the page loads.
let count = Number(localStorage.getItem("count") ?? 0);
One way to solve it. Yours can look different and still pass the checks.
const output = document.querySelector("#count");
let count = Number(localStorage.getItem("count") ?? 0);
output.textContent = count;
document.querySelector("#more").addEventListener("click", () => {
count = count + 1;
localStorage.setItem("count", count);
output.textContent = count;
});
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const output = document.querySelector("#count");
let count = localStorage.getItem("count") ?? 0;
output.textContent = count;
document.querySelector("#more").addEventListener("click", () => {
count = count + 1;
localStorage.setItem("count", count);
output.textContent = count;
});
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const text = (page, selector) => page.document.querySelector(selector).textContent;
const click = (page, selector) => page.document.querySelector(selector).click();
test('A new page starts at 0 and counts to 1', async () => {
const page = await loadPage();
assert.equal(text(page, '#count'), '0', `at the start the page shows ${text(page, '#count')}`);
click(page, '#more');
assert.equal(text(page, '#count'), '1', `after one click the page shows ${text(page, '#count')}`);
});
test('A saved count of 4 goes on with 5', async () => {
const page = await loadPage({localStorage: {count: '4'}});
click(page, '#more');
assert.equal(text(page, '#count'), '5', `after one click the page shows ${text(page, '#count')}`);
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Counter</title>
</head>
<body>
<p id="count"></p>
<button id="more" type="button">+1</button>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Run the checks (needs learnrun.js in the same folder):
npm install jsdom@28.1.0
node --testDownload learnrun.jsconst books = JSON.parse(localStorage.getItem("books"));
document.querySelector("#out").textContent = books.length + " books";
What Node.js prints
TypeError: Cannot read properties of null (reading 'length')Why, and the fix
On a first visit nothing is saved yet. getItem gives null, and JSON.parse(null) is null too, without an error, so the crash comes one line later. Give a default: const books = JSON.parse(localStorage.getItem("books")) ?? [];.
localStorage.setItem("user", {name: "Ada"});
const user = JSON.parse(localStorage.getItem("user"));
document.querySelector("#out").textContent = user.name;
What Node.js prints
SyntaxError: "[object Object]" is not valid JSONWhy, and the fix
setItem stores strings only, so the object became the text "[object Object]", which is not JSON. Save it with localStorage.setItem("user", JSON.stringify(user)), and keep JSON.parse for loading.
localStorage.setItem("theme", "dark");
console.log(localStorage.getItem("theme"));
What Node.js prints
ReferenceError: localStorage is not definedWhy, and the fix
localStorage belongs to the browser. In Node.js 24 it exists only when node is started with --experimental-webstorage, so node main.js does not know it. Run storage code in a page (index.html with main.js, from a local web server); in Node.js, keep data in a file with node:fs instead.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.