Chapter 4 · Real programs
Exceptions
Step 1 of 22 · Concept
Fail well
By the end of this lesson your programs fail well. They say what went wrong, and carry on where they can. You will:
- refuse with an error, and catch it,
- make your own kind of error,
- survive bad input and missing files.
Step 2 of 22 · Concept
throw stops the function and every caller
Lesson 17's withdraw printed Refused and went on. Now it refuses with an error, in the file account.js:
withdraw(amount) {
if (amount > this.balance) {
throw new Error(
`balance is ${this.balance}, asked for ${amount}`
);
}
this.balance -= amount;
}
throw new Error("...") stops withdraw at once: no return, no next line. Its caller stops too, and that one's
caller, until a try catches the error.
Step 3 of 22 · Demo
An uncaught error ends the program
Run it. The error stops withdraw, then the module's own lines: Balance: never prints. Open account.js to see
the throw.
Step 4 of 22 · Concept
try runs it, catch takes the error
try {
// lines that may throw
} catch (e) {
// runs only when one of them threw
}
When a line in try throws, the rest of try is skipped and catch runs, with the error in e. Then the program
goes on after the catch. e.message is the text of the error, and e.name its kind, "Error".
Step 5 of 22 · Quiz
Does the next line run?
account.js is the file of the step throw stops the function and every caller. Type the missing line.
import { Account } from "./account.js";
const account = new Account("Ada");
account.deposit(5);
try {
account.withdraw(10);
console.log("after withdraw");
} catch (e) {
console.log(e.message);
}
console.log("done");
Step 6 of 22 · Demo
See it run
Run it and compare with your answer. Then change 10 to 3 and run it again: no error, so catch is skipped.
Step 7 of 22 · Trace
Watch the error leave the frames
main calls transfer, and transfer calls withdraw, which throws. Press Next and watch the stack.
function withdraw(balance, amount) {
if (amount > balance) {
throw new Error("too much");
}
return balance - amount;
}
function transfer(amount) {
const left = withdraw(5, amount);
console.log("Sent", amount);
return left;
}
function main() {
try {
transfer(10);
} catch (e) {
console.log("Refused:", e.message);
}
console.log("End");
}
main();mainis in itstry. Line 14 is next: it callstransfer(10).- A frame for
transfergoes on the stack, withamount10. Line 8 callswithdraw. - A frame for
withdrawgoes on top. 10 is more than 5, so line 3 is next: it throws. withdraw's frame is removed at once. The error arrives intransferat line 8, soleftis never set.transferhas nocatch, so its frame goes too: line 9 never runs.main'scatchis next, line 16.- The
catchprintedRefused: too much. The program goes on after it, at line 18. - Line 18 printed
End.mainreturns, and the program is finished.
Step 8 of 22 · Exercise
Your turn: refuse without stopping
Three withdrawals, and the second is too big. Catch its error inside the loop, log Refused: and the message, and
go on. The program must print:
Refused: balance is 5, asked for 10
Balance: 2
A try around the whole loop stops the loop at the first error: the third withdrawal never runs, and the balance stays 5. Put the try inside the loop.
Inside the loop's block: try { ... } around account.withdraw(amount);, then a catch (e) that logs the message in a template literal.
} catch (e) {
console.log(`Refused: ${e.message}`);
}
Step 9 of 22 · Concept
Your own kind of error
class InsufficientFunds extends Error {
constructor(message) {
super(message);
this.name = "InsufficientFunds";
}
}
const e = new InsufficientFunds("balance is 5, asked for 10");
extends Error links the class to Error.prototype (lesson 17), so e instanceof Error is true. super(message)
runs Error's constructor, which sets message on the new object itself. this.name shadows the name of
Error.prototype, which is "Error".
Step 10 of 22 · Quiz
Which line will Node print?
This class has no this.name. Which line of its error does Node print?
class InsufficientFunds extends Error {
constructor(message) {
super(message);
}
}
throw new InsufficientFunds("balance is 5, asked for 10");
Step 11 of 22 · Demo
See it run
Run it and compare with your answer. Then put this.name = "InsufficientFunds"; after super(message); and run it
again: the line now reads InsufficientFunds: balance is 5, asked for 10.
Step 12 of 22 · Concept
Catch what you expect, throw the rest
A catch gets every error of its try, a typo's TypeError too. So check the kind with instanceof (lesson 17).
Throw the others again:
} catch (e) {
if (e instanceof InsufficientFunds) {
console.log("Refused:", e.message);
} else {
throw e;
}
}
throw e sends the error on to the callers, as if nobody had caught it. A bug then stops the program with its own
message. It does not hide as "refused".
Step 13 of 22 · Demo
A bug is not a refusal
There is no account for Bo, so account is undefined (lesson 13). Run it. The error is a TypeError, so
throw e ends the program with Node's message.
Step 14 of 22 · Exercise
Your turn: an error of your own
- Write
class TooYoung extends Error, with its ownname. checkAge(age)throws aTooYoungwith the message`${age} is under 13`whenageis under 13.- In the loop, catch a
TooYoungand log its name and message; throw any other error again.
The program must print:
Welcome, 15
TooYoung: 11 is under 13
Forget this.name and the line reads Error: 11 is under 13: the name comes from Error.prototype. Set it in the constructor, after super(message).
The class is InsufficientFunds of the step Your own kind of error, renamed. In checkAge, an if (age < 13) throws; in the loop, a try with a catch as in the step Catch what you expect, throw the rest.
throw new TooYoung(`${age} is under 13`);
Step 15 of 22 · Concept
Some errors come from outside
Your code can be right and still meet bad text or a missing file. Two you will meet often:
| this | throws | its message |
|---|---|---|
JSON.parse("abc") | a SyntaxError | Unexpected token 'a', "abc" is not valid JSON |
readFileSync("data.txt", "utf8") | an Error whose code is "ENOENT" | ENOENT: no such file or directory, open 'data.txt' |
ENOENT means "no such entry": no file of that name. A JSON.parse SyntaxError comes while the program runs, not
before it starts as a typo's does (lesson 5).
Step 16 of 22 · Quiz
Which text is not JSON?
Which of these texts makes JSON.parse throw?
Step 17 of 22 · Demo
Catch them both
Each risky call has its own try. Run it: the SyntaxError's name and message, the missing file's code, and
last a check that Number("abc") is NaN.
Step 18 of 22 · Demo
finally runs either way
A finally block runs after try, whether it threw or not, even after a return. Run it: finally prints for the
good text and for the bad one.
Step 19 of 22 · Concept
Check first, or try first
Some mistakes are easy to test before you go on; others only the work itself can find.
| when | do | for example |
|---|---|---|
| a test is easy | check first, with an if | Number.isNaN(n) after Number(text) |
| only the work can tell | try first, and catch | JSON.parse(text), readFileSync(name, "utf8") |
Number.isNaN(x) is true only for NaN. x === NaN is always false, even for NaN, so never compare with it.
Step 20 of 22 · Check yourself
Check yourself: read the settings
Each input line is a settings text in JSON. For each line:
JSON.parseit inside atry.- Make
settings.volumea number; when it isNaN, thrownew Error("volume must be a number"). - Log
Volume: 7, orSettings refused:and the error's message.
The program must print:
Volume: 7
Settings refused: volume must be a number
Settings refused: Expected property name or '}' in JSON at position 1 (line 1 column 2)
One try holds all three steps, and one catch logs every message: your own error's and the SyntaxError's alike.
Step 21 of 22 · Challenge · optional
Challenge: say how much is missing
Optional.
Give InsufficientFunds a second argument, missing, and keep it on the error. Then withdraw(5, 10) can say how
much was missing. The program must print:
Missing: 5
An error is an object: it can keep a property of its own, as an Account keeps owner. Right now e.missing is undefined.
The constructor takes (message, missing) and sets this.missing = missing; after super(message). The throw passes a second argument.
amount - balance
Step 22 of 22 · Recap
Recap
throw new Error("...")stops the function and every caller, until atrycatches it.- In
try, a line that throws skips the rest, andcatch (e)gets the error. - An error class
extends Errorand setsthis.name; catch it withinstanceof. JSON.parsethrows aSyntaxError; a missing file'scodeis"ENOENT".- Check first when a test is easy (
Number.isNaN); try first when only the work can tell.
Next: lesson 19 waits for timers and files without stopping the program.
Review
3 questions from this lesson, in a new order each time. Answer from memory: 2 of 3 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
import { Account } from "./account.js";
const account = new Account("Ada");
account.deposit(5);
console.log("Asking for 10");
account.withdraw(10);
console.log("Balance:", account.balance);
import { Account } from "./account.js";
const account = new Account("Ada");
account.deposit(5);
try {
account.withdraw(10);
console.log("after withdraw");
} catch (e) {
console.log(e.message);
}
console.log("done");
import { Account } from "./account.js";
const account = new Account("Ada");
account.deposit(8);
for (const amount of [3, 10, 3]) {
account.withdraw(amount);
}
console.log(`Balance: ${account.balance}`);
class InsufficientFunds extends Error {
constructor(message) {
super(message);
}
}
throw new InsufficientFunds("balance is 5, asked for 10");
class InsufficientFunds extends Error {
constructor(message) {
super(message);
this.name = "InsufficientFunds";
}
}
const accounts = new Map();
const account = accounts.get("Bo");
try {
account.withdraw(10);
} catch (e) {
if (e instanceof InsufficientFunds) {
console.log("Refused:", e.message);
} else {
throw e;
}
}
// 1. class TooYoung extends Error, with its name
function checkAge(age) {
// 2. under 13: throw a TooYoung
console.log(`Welcome, ${age}`);
}
for (const age of [15, 11]) {
checkAge(age);
// 3. catch a TooYoung: log its name and message
}
import { readFileSync } from "node:fs";
try {
JSON.parse("abc");
} catch (e) {
console.log(e.name);
console.log(e.message);
}
try {
readFileSync("data.txt", "utf8");
} catch (e) {
console.log(e.code);
}
console.log(Number.isNaN(Number("abc")));
function load(text) {
try {
console.log("parsing");
return JSON.parse(text);
} catch (e) {
console.log("caught:", e.name);
return null;
} finally {
console.log("finally");
}
}
console.log(load("[1]"));
console.log(load("abc"));
import { readFileSync } from "node:fs";
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
// 1. parse the line in a try
// 2. a number, or throw volume must be a number
// 3. log Volume: ..., or Settings refused: ...
}
class InsufficientFunds extends Error {
constructor(message) {
super(message);
this.name = "InsufficientFunds";
}
}
function withdraw(balance, amount) {
if (amount > balance) {
throw new InsufficientFunds(
`balance is ${balance}, asked for ${amount}`
);
}
return balance - amount;
}
try {
withdraw(5, 10);
} catch (e) {
if (e instanceof InsufficientFunds) {
console.log(`Missing: ${e.missing}`);
} else {
throw e;
}
}