11. Errors and exceptions
A function that cannot do its job should say so loudly, not quietly return nonsense. Exceptions let a problem deep in the call stack jump straight to the code that knows how to handle it.
- Signal a problem with throw new Error(...), and explain why that beats returning a strange value.
- Handle errors with try, catch and finally, and say when each block runs.
- Read an error's name and message, and make your own error class.
- Trace a thrown error as it unwinds the call stack to the nearest catch.
A quiet wrong answer
In the last chapter you turned digit strings into numbers. Here’s that function again:
function toNumber(s) {
let n = 0;
for (let i = 0; i < s.length; i++) {
n = n * 10 + (s.charCodeAt(i) - 48);
}
return n;
}
console.log(toNumber('42')); // 42
console.log(toNumber('4x')); // 112 (!)
'4x' isn’t a number at all, but the function happily answers 112,
because 'x' has code 120, and 120 − 48 is 72. Nothing crashes. The
nonsense value just flows on into the rest of the program, and the bug
shows up somewhere far away, much later, looking like something else.
A quiet wrong answer is the worst kind of bug. What toNumber needs is a
way to refuse: “I can’t do this, and here’s why.”
Throwing an error is pulling the fire alarm. Everyone stops what they’re doing, mid-sentence, and leaves. They don’t go back to finish their work. Someone whose job is to deal with fires takes over. If nobody has that job, the whole building shuts down.
throw
function toNumber(s) {
let n = 0;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 48 || c > 57) {
throw new Error(`bad digit at ${i}`);
}
n = n * 10 + (c - 48);
}
return n;
}
toNumber('4x');
// Error: bad digit at 1
(|| means “or”: true if either side is true.) throw stops the
function on the spot. It doesn’t return anything, not even undefined.
If nothing handles the error, the whole program stops and the error is
reported, so you find out about the problem exactly where it happened.
throw value stops the current code immediately and raises an
exception. You can throw any value, but the convention is to throw
an Error object, made with new Error(message). An exception that
nothing handles stops the program[1].
try and catch
Sometimes a problem is expected, and you know what to do about it.
Wrap the risky code in try, and say what to do if it throws in
catch:
try {
const n = toNumber('4x');
console.log('got', n); // skipped
} catch (e) {
console.log('oops: ' + e.message);
} finally {
console.log('done');
}
// oops: bad digit at 1
// done
The moment toNumber throws, the rest of the try block is skipped and
control jumps into catch. The thrown value arrives as e. If nothing
throws, catch is skipped completely. And finally runs either way,
which makes it the place for clean-up that must always happen.
A try/catch statement runs the try block. If anything in it throws,
the rest of the block is skipped and the catch (e) block runs, with e
holding the thrown value. An optional finally block runs afterwards,
whether or not anything was thrown[1].
function check(n) {
if (n < 0) {
throw new Error('negative: ' + n);
}
return n;
}
try {
console.log(check(3));
console.log(check(-1));
console.log('not reached');
} catch (e) {
console.log(e.name, e.message);
} finally {
console.log('finally');
}
console.log('carrying on');An empty catch {} swallows every error, including bugs you didn’t
expect, and brings back the quiet wrong answer. Catch only where you can
genuinely do something useful, such as showing the user a message.
What’s inside an Error
An Error object is an ordinary object with two properties you’ll use
constantly: message, the text you gave it, and name, the kind of
error[2]. JavaScript itself throws several kinds.
You’ve already met some of them:
null.x; // TypeError
missing + 1; // ReferenceError
forever(0); // RangeError (chapter 9)
let 1x = 0; // SyntaxError
You can make your own kinds of error with a class. A scanner, for example, wants to say where the problem is:
class ParseError extends Error {
constructor(message, pos) {
super(message);
this.name = 'ParseError';
this.pos = pos;
}
}
Two new words here. extends Error makes ParseError a special kind of
Error: it gets everything an Error has. And super(message) runs
Error’s own constructor first, so the message is set up properly. Then
instanceof lets a catch tell the kinds apart:
try {
throw new ParseError('bad digit', 1);
} catch (e) {
if (e instanceof ParseError) {
console.log(`${e.message} at ${e.pos}`);
} else {
throw e; // not ours: pass it on
}
}
That last line matters. A catch that doesn’t recognize an error can
rethrow it, so it keeps travelling to code that does.
An Error object has a name (like 'TypeError') and a message.
new Error(message) makes one. A custom error class
(class ParseError extends Error) adds its own name and extra
properties, and instanceof tells kinds apart in a catch.
How an error travels
When something throws, where does it go? Not just to the function that
called it. It goes back down the call stack, frame by frame, until it
finds a try. Step through it:
function digit(c) {if (c < 48) {throw new Error('bad digit');}return c - 48;}function parse(s) {return digit(s.charCodeAt(0)) + 1;}try {console.log(parse('+'));} catch (e) {console.log('caught: ' + e.message);}
Error propagation: a thrown error unwinds the call stack. Each
frame without a surrounding try is popped immediately, without
returning and without finishing its line. The first frame inside a try
catches it. If the error unwinds all the way to the bottom of the stack,
the program stops.
This is what makes exceptions so useful. The function that notices a
problem (digit) is often deep in the stack, and has no idea what to
do about it. The code that knows what to do (show a message, skip one
line of input) is often far below. Throwing connects the two without
every function in between having to check for and pass along error
values.
Your Drip interpreter in Part III will throw errors of its own: syntax
errors from the parser, and runtime errors such as adding a number to a
string. Each will carry a line and column, like the pos above. You’ll
also see that exceptions are one way to make Drip’s return jump out of
several nested statements at once. Most engines also give Error objects a
stack property listing the calls that were active, which is what you
see in browser consoles. It isn’t part of the language standard, though,
so its contents differ between engines[2]. In
Part IV, your VM keeps its own frames in an array, so it will have to do
the unwinding by hand.
Your turn
Write parseDigits(s): like toNumber, it turns a string of digits into a number. But it must throw an Error instead of returning nonsense:
- if
sis empty, throw an Error with the message'empty number'; - if the character at index
iisn't a digit, throw an Error with the message'bad digit at 'followed byi(soparseDigits('4x')throwsbad digit at 1).
function parseDigits(s) {
// your code here
}
console.log(parseDigits('42'));Write attempt(f). It calls the function f with no arguments. If f returns a value v, attempt returns { ok: true, value: v }. If f throws an Error, attempt returns { ok: false, error: message }, where message is the error’s message. attempt itself must never throw.
function attempt(f) {
// your code here
}
console.log(attempt(() => 42));What you learned
throw new Error(message)stops the code immediately. It’s far better than returning a quiet wrong answer.- try/catch handles errors: if the
tryblock throws, the rest of it is skipped andcatch (e)runs.finallyruns either way. - An Error object has a
nameand amessage. Custom error classesextends Errorand carry extra details, like a position. - A thrown error propagates: it unwinds the call stack, popping
frames, until it reaches the nearest
try. Unhandled, it stops the program.
Last chapter of the day: splitting a program across files.
Check your understanding
0/6- 1.
What does this print?
try { console.log('a'); throw new Error('b'); console.log('c'); } catch (e) { console.log(e.message); } - 2.
When does a
finallyblock run? - 3.
a()callsb(), which callsc(), which throws. Onlyahas atry/catcharound its call tob. What happens? - 4.
You
throw 'oops'(a string) and catch it ase. What ise.message? - 5.
What does
e instanceof ParseErrorlet acatchblock do? - 6.
Why does a parser usually throw an error on bad input, rather than return a special value?
References
- [1] MDN contributors. Control flow and error handling (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling (checked 2026-10-31)
- [2] MDN contributors. Error. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error (checked 2026-10-31)