5.8 — Error handling
Lesson 0.5 taught you to read errors — the machine's notes asking for help. Every error you've met since was thrown at you by the engine. Today the tables turn: you throw them — and you catch them.
Why would code deliberately crash? Because "limp onward with garbage" is worse: a withdraw(-500) that quietly proceeds corrupts an account; one that throws stops the wrongness at its source (5.2's loud-beats-silent philosophy, now as a tool you wield).
And here's the career secret hiding in this lesson: a failing test assertion is a throw — expect throws, the runner catches. You're about to learn the exact mechanism your future job runs on.
function withdraw(amount) {
if (amount > 100) {
throw new Error("limit is 100");
}
return "you got " + amount;
}
try {
console.log(withdraw(50));
console.log(withdraw(500));
console.log("never reached");
} catch (err) {
console.log("caught: " + err.message);
} finally {
console.log("receipt printed");
}The try block starts running its lines normally. withdraw(50) passes the guard, returns "you got 50", printed. A try block costs nothing when nothing goes wrong — it’s just a marked stretch of road with a safety net below.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
Error is a class with a family tree — 5.6's chains, live. TypeError, ReferenceError, SyntaxError all extends Error. That is why every error you've read since 0.5 had the same shape: .name, .message, .stack.
You can extend it too: class PaymentError extends Error lets a catch block tell YOUR errors apart with instanceof (5.6's chain question, at work).
Throw stops functions the way return does, but travels differently: return hands a value to the immediate caller; throw keeps falling through callers until a net appears.
Catch what you can handle, let the rest keep falling — catching everything and doing nothing ("swallowing") turns loud bugs back into silent ones, undoing the whole point.
One honest boundary: try/catch guards the synchronous stack. Errors born inside callbacks that run later (a setTimeout, a network reply) happen after your try has already finished. Catching those needs Phase 6's machinery — promises carry their errors, and await makes try/catch work on them again. Foreshadowed, on purpose.
⌨️ the division desk with a complaints window
Build a function that refuses impossible work by THROWING — and a caller that survives the refusal gracefully and always signs off.
requirements:
- A function
divide(a, b): ifbis exactly0, THROW a newErrorwith the message"cannot divide by zero". Otherwise returna / b. - In a
try: printdivide(10, 2), then printdivide(5, 0). - In the
catch: print the caught error's.message(just the message). In afinally: print"done".
when you press RUN, the console must show exactly:
✏️ Quick check 1
Does "B" ever print? Type yes or no:
try {
throw new Error("stop");
console.log("B");
} catch (e) {
console.log("C");
}✏️ Quick check 2
Type exactly what this prints:
try {
throw new Error("oven too hot");
} catch (e) {
console.log(e.message);
}✏️ Quick check 3
A try block succeeds with no error at all. Does its finally still run? Type yes or no:
🗣️ Now teach it back
Explain to a friend what happens from the instant `throw` fires to the moment life is normal again — the stopping, the falling, the net, the finally — and why a failing test in a big suite doesn’t crash the other tests.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.