Warm-up · Activity 1 of 7
Warm-up from lesson I4.1: a built-in error type. What does this print?
const list = [];
try {
list.length = -1;
} catch (error) {
console.log(error.name);
}// I4.2 · ~34 min · Intermediate
After this lesson you can give your program its own error types, decide by type instead of by message text, and keep the original error when you add context.
You will be able to
Warm-up · Activity 1 of 7
const list = [];
try {
list.length = -1;
} catch (error) {
console.log(error.name);
}Predict · Activity 2 of 7
class NotFoundError extends Error {}
const error = new NotFoundError("No user with id 2");
console.log(error.name, error instanceof NotFoundError);Practice · Activity 3 of 7
super(message, ____);Practice · Activity 4 of 7
class AppError extends Error {}
class DbError extends AppError {}
const error = new DbError("Connection lost");
console.log(error instanceof AppError, error instanceof TypeError);Practice · Activity 5 of 7
const low = new Error("ECONNREFUSED");
const high = new Error("Could not load users", {cause: low});
console.log(high.message + " / " + high.cause.message);Brain teaser · Activity 6 of 7
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
console.log(JSON.stringify(new ValidationError("email", "Email is missing")));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
NotFoundError carries the id that was not found, and the loop handles only that type, so any other error would still stop the program. ValidationError shows the full pattern: options passed to super, a name, and a field. parseSettings wraps the SyntaxError of JSON.parse in an error that says what the program was doing.
main.js
class ValidationError extends Error {
constructor(field, message, options) {
super(message, options); // first: sets message, and cause if given
this.name = "ValidationError";
this.field = field;
}
}
class NotFoundError extends Error {
constructor(id) {
super(`No user with id ${id}`);
this.name = "NotFoundError";
this.id = id;
}
}
const users = new Map([[1, {name: "Ada"}]]);
function findUser(id) {
const user = users.get(id);
if (!user) throw new NotFoundError(id);
return user;
}
function parseSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error("Could not read the settings", {cause: error});
}
}
for (const id of [1, 2]) {
try {
console.log(findUser(id).name);
} catch (error) {
if (error instanceof NotFoundError) console.log("404:", error.message);
else throw error;
}
}
const invalid = new ValidationError("email", "Email is missing");
console.log(String(invalid), invalid.field, invalid instanceof Error);
try {
parseSettings("{oops");
} catch (error) {
console.log(error.message, "<-", error.cause.name);
}
Run it with
node main.jsOutput
Ada
404: No user with id 2
ValidationError: Email is missing email true
Could not read the settings <- SyntaxErrorTab 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 2
HttpError should carry the HTTP status of a failed request: new HttpError(503, "Service unavailable") has the name HttpError, the message, and a status field. isRetryable(error) is true only for an HttpError with a status of 500 or more. The starter’s class sets neither name nor status.
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.
Run the tests: what does String(error) show as the name, and what is error.status?
After super(message), set this.name and this.status.
Pass options on too, so an HttpError can carry a cause.
One way to solve it. Yours can look different and still pass the checks.
export class HttpError extends Error {
constructor(status, message, options) {
super(message, options);
this.name = "HttpError";
this.status = status;
}
}
export function isRetryable(error) {
return error instanceof HttpError && error.status >= 500;
}
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 class HttpError extends Error {
constructor(status, message) {
super(message);
}
}
export function isRetryable(error) {
return error instanceof HttpError && error.status >= 500;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {HttpError, isRetryable} from './main.js';
test('an HttpError has its name, message and status', () => {
const error = new HttpError(503, 'Service unavailable');
assert.equal(String(error), 'HttpError: Service unavailable', `String(error) is ${String(error)}`);
assert.equal(error.status, 503, `error.status is ${error.status}`);
assert.ok(error instanceof Error, 'an HttpError should be an Error');
});
test('a 503 can be retried, a 404 cannot', () => {
assert.equal(isRetryable(new HttpError(503, 'Service unavailable')), true, 'a 503 should be retryable');
assert.equal(isRetryable(new HttpError(404, 'Not found')), false, 'a 404 should not be retryable');
});
test('other errors are never retried', () => {
assert.equal(isRetryable(new TypeError('x is not a function')), false, 'a TypeError should not be retryable');
});
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 2
loadConfig(text) parses a config written as JSON. For broken JSON it throws a ConfigError with the message Config is not valid JSON and the SyntaxError as its cause. loadConfig already passes {cause: error}, but the cause is lost on the way. Find where, and fix it.
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.
Who reads the options object with the cause in it?
The constructor of ConfigError never passes the second argument on to Error.
constructor(message, options) { super(message, options); … }
One way to solve it. Yours can look different and still pass the checks.
export class ConfigError extends Error {
constructor(message, options) {
super(message, options);
this.name = "ConfigError";
}
}
export function loadConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new ConfigError("Config is not valid JSON", {cause: 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 class ConfigError extends Error {
constructor(message) {
super(message);
this.name = "ConfigError";
}
}
export function loadConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new ConfigError("Config is not valid JSON", {cause: error});
}
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {ConfigError, loadConfig} from './main.js';
test('valid JSON comes back as an object', () => {
assert.deepEqual(loadConfig('{"port": 8080}'), {port: 8080}, 'loadConfig should parse valid JSON');
});
test('broken JSON throws a ConfigError', () => {
assert.throws(() => loadConfig('{port'), ConfigError, 'loadConfig should throw a ConfigError');
});
test('the ConfigError keeps the SyntaxError as its cause', () => {
try {
loadConfig('{port');
assert.fail('loadConfig did not throw');
} catch (error) {
assert.ok(error.cause instanceof SyntaxError, `error.cause is ${error.cause}`);
}
});
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.jsclass ValidationError extends Error {
constructor(field, message) {
this.field = field;
super(message);
}
}
new ValidationError("email", "Email is missing");
What Node.js prints
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorWhy, and the fix
In a class that extends another, the object is created by the parent constructor, so this does not exist before super() has run. Call super(message, options) as the first line of the constructor, then set name and the other fields.
class LoadError extends Error {
constructor(message) {
super(message);
this.name = "LoadError";
}
}
try {
throw new LoadError("Could not load", {cause: new Error("timeout")});
} catch (error) {
console.log(error.cause.message);
}
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'message')Why, and the fix
Error only sets cause from the options object it receives, and this constructor never passes it on, so error.cause is undefined. Accept options and hand them to super: constructor(message, options) { super(message, options); … }.
class NotFoundError extends Error {
constructor(id) {
super(`No user with id ${id}`);
this.name = "NotFoundError";
}
}
throw NotFoundError(2);
What Node.js prints
TypeError: Class constructor NotFoundError cannot be invoked without 'new'Why, and the fix
The built-in Error() also works without new, but a class never does. Write throw new NotFoundError(2).
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.