Warm-up · Activity 1 of 7
Warm-up from lesson I3.5: an empty JSON file. What does this print?
try {
JSON.parse("");
} catch (error) {
console.log(error.name);
}// I4.1 · ~38 min · Intermediate
After this lesson you can make your functions fail loudly with an Error, handle the errors you expect, and let the others through.
Lesson 1 of 5 in I4 Errors, debugging and testing
You will be able to
Warm-up · Activity 1 of 7
try {
JSON.parse("");
} catch (error) {
console.log(error.name);
}Predict · Activity 2 of 7
try {
console.log("A");
throw new Error("B");
console.log("C");
} catch (error) {
console.log(error.message);
} finally {
console.log("D");
}
console.log("E");Practice · Activity 3 of 7
if (error ____ RangeError) console.log("range");Practice · Activity 4 of 7
function load() {
try {
return "data";
} finally {
console.log("cleanup");
}
}
console.log(load());Practice · Activity 5 of 7
function parse(text) {
try {
return JSON.parse(text).items.length;
} catch (error) {
if (error instanceof SyntaxError) return 0;
throw error;
}
}
try {
console.log(parse("{}"));
} catch (error) {
console.log("outer:", error.name);
}Brain teaser · Activity 6 of 7
function check() {
try {
throw new Error("broken");
} catch (error) {
return "caught " + error.message;
} finally {
return "finally wins";
}
}
console.log(check());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
parseAge throws a TypeError for text that is not a whole number and a RangeError for an age out of range. The loop catches each error, so one bad input does not stop the others, and finally reports every input. readSettings handles broken JSON with defaults and rethrows every other error.
main.js
function parseAge(text) {
const age = Number(text);
if (!Number.isInteger(age)) throw new TypeError(`Not a whole number: "${text}"`);
if (age < 0 || age > 150) throw new RangeError(`Out of range: ${age}`);
return age; // only reached when both checks pass
}
for (const input of ["42", "abc", "-3"]) {
try {
console.log("age", parseAge(input));
} catch (error) {
console.log(error.name + ": " + error.message);
} finally {
console.log("checked", input);
}
}
function readSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
if (error instanceof SyntaxError) return {}; // broken JSON: use defaults
throw error; // anything else is not ours to handle
}
}
console.log(readSettings('{"theme": "dark"}'), readSettings("{oops"));
Run it with
node main.jsOutput
age 42
checked 42
TypeError: Not a whole number: "abc"
checked abc
RangeError: Out of range: -3
checked -3
{ theme: 'dark' } {}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.
Exercise 1 of 3
parsePrice(text) turns text such as "12.50" into a number. For text that is not a number, or empty, it must throw a TypeError with the message Not a price: "…" (the text in quotes), and for a negative number a RangeError. The starter returns whatever Number gives, so bad input slips through as NaN or 0.
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.
Number("abc") is NaN, and Number("") is 0: check both before you return.
Number.isNaN(price) and text.trim() === "" find the two bad cases.
throw new TypeError(`Not a price: "${text}"`)
One way to solve it. Yours can look different and still pass the checks.
export function parsePrice(text) {
const price = Number(text);
if (text.trim() === "" || Number.isNaN(price)) throw new TypeError(`Not a price: "${text}"`);
if (price < 0) throw new RangeError(`Negative price: ${price}`);
return price;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function parsePrice(text) {
return Number(text);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parsePrice} from './main.js';
test('12.50 is 12.5', () => {
assert.equal(parsePrice('12.50'), 12.5, 'parsePrice("12.50") should be 12.5');
});
test('abc throws a TypeError with the text in the message', () => {
assert.throws(() => parsePrice('abc'), {name: 'TypeError', message: 'Not a price: "abc"'}, 'parsePrice("abc") should throw TypeError: Not a price: "abc"');
});
test('an empty text throws a TypeError, not 0', () => {
assert.throws(() => parsePrice(''), {name: 'TypeError'}, 'parsePrice("") should throw a TypeError, but Number("") is 0');
});
test('a negative price throws a RangeError', () => {
assert.throws(() => parsePrice('-1'), {name: 'RangeError'}, 'parsePrice("-1") should throw a RangeError');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
loadCart(text) reads a cart saved as JSON: an array of items. Broken JSON should give an empty cart, but valid JSON that is not an array is a bug elsewhere and must throw the TypeError Cart must be an array. The starter’s catch swallows every error, so that TypeError never reaches the caller. Handle only SyntaxError.
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.
Give the catch block a name for the error, so you can look at it.
error instanceof SyntaxError is true only for broken JSON.
For any other error: throw error;
One way to solve it. Yours can look different and still pass the checks.
export function loadCart(text) {
try {
const cart = JSON.parse(text);
if (!Array.isArray(cart)) throw new TypeError("Cart must be an array");
return cart;
} catch (error) {
if (error instanceof SyntaxError) return [];
throw error;
}
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function loadCart(text) {
try {
const cart = JSON.parse(text);
if (!Array.isArray(cart)) throw new TypeError("Cart must be an array");
return cart;
} catch {
return [];
}
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadCart} from './main.js';
test('a saved array comes back', () => {
const cart = loadCart('[{"id": 1}]');
assert.deepEqual(cart, [{id: 1}], `loadCart gave ${JSON.stringify(cart)}`);
});
test('broken JSON gives an empty cart', () => {
const cart = loadCart('[{"id": 1');
assert.deepEqual(cart, [], `loadCart gave ${JSON.stringify(cart)}`);
});
test('an object instead of an array throws the TypeError', () => {
assert.throws(() => loadCart('{"id": 1}'), {name: 'TypeError', message: 'Cart must be an array'}, 'loadCart should not swallow the TypeError');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
runLocked(lock, task) sets lock.busy to true, runs task() and returns its result. Afterwards lock.busy must be false again, also when task throws; the error itself must still reach the caller. The starter only unlocks after a successful task, so one failed task leaves the lock busy for ever.
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.
Which block runs both after success and after an error?
You do not need catch here: try...finally lets the error through.
try { return task(); } finally { lock.busy = false; }
One way to solve it. Yours can look different and still pass the checks.
export function runLocked(lock, task) {
lock.busy = true;
try {
return task();
} finally {
lock.busy = false;
}
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function runLocked(lock, task) {
lock.busy = true;
const result = task();
lock.busy = false;
return result;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runLocked} from './main.js';
test('returns the result and unlocks', () => {
const lock = {busy: false};
assert.equal(runLocked(lock, () => 42), 42, 'runLocked should return what the task returns');
assert.equal(lock.busy, false, 'lock.busy should be false after the task');
});
test('the task sees the lock as busy', () => {
const lock = {busy: false};
assert.equal(runLocked(lock, () => lock.busy), true, 'lock.busy should be true while the task runs');
});
test('a failing task still unlocks, and its error reaches the caller', () => {
const lock = {busy: false};
assert.throws(() => runLocked(lock, () => { throw new Error('disk full'); }), {message: 'disk full'}, 'the error of the task should reach the caller');
assert.equal(lock.busy, false, 'lock.busy should be false after a failed task');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsfunction findUser(id) {
throw "User " + id + " not found";
}
try {
findUser(7);
} catch (error) {
console.log(error.message.toUpperCase());
}
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'toUpperCase')Why, and the fix
A thrown string has no message, name or stack, so code that expects an Error breaks, and you lose the place where it happened. Throw an Error object: throw new Error(`User ${id} not found`). Use a built-in type, such as TypeError or RangeError, when it fits.
try {
const settings = JSON.parse('{"theme": "dark"}');
} catch (error) {
console.log("broken settings");
}
console.log(settings.theme);
What Node.js prints
ReferenceError: settings is not definedWhy, and the fix
const and let inside the try block exist only in that block. Use the value inside try, or declare it before: let settings = {}; then try { settings = JSON.parse(text); } catch { … }, so it also has a value after an error.
let settings;
try {
settings = JSON.parse("{oops");
} catch (error) {
console.log("could not read the settings");
}
console.log(settings.theme);
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'theme')Why, and the fix
The catch block printed a line, but left settings undefined, so the program fails a few lines later with a less helpful error. A catch must leave the program in a usable state: give settings a default value there, or rethrow the error if the program cannot go on.
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.