Module 10 ¡ Error Handling: throw, try, catch, finally, and Reading a Stack Trace
Errors, the Picture: throw, catch, and a Call Stack That Unwinds
In this lesson
- Throw an Error from a function, and catch it with
tryandcatch, so one bad input does not end the whole program. - Picture a throw climbing the call stack, frame by frame, until a
catchtakes it. - Read an Error's name, message and stack, and explain why you throw an Error and not a string.
Sadia runs a bakery that takes bulk orders. Every evening her staff type the next day's quantities into a sheet, and a small program adds them up for the oven. Last Tuesday someone typed 1o instead of 10, with the letter o. The program did not complain. It printed a total of NaN, the oven plan said nothing useful, and nobody noticed until morning.
Sadia's nephew fixed it the quick way: the program now stops at the first bad entry. That was worse. One typo now hides every other entry, good ones included. What Sadia really wants is in between. A bad entry should be named, loudly, and the rest of the sheet should still be counted. That is exactly what errors are for, and this module teaches them properly.
When a value is wrong, JavaScript often keeps going
Many operations in JavaScript do not stop on a bad value. They give a strange value instead and carry on. Number("1o") is NaN, and NaN plus anything is NaN. Module 2 showed this.
const sheet = ["12", "1o", "7"];
let total = 0;
for (const entry of sheet) total += Number(entry);
console.log(total);
console.log(Number("1o"));
NaN
NaN
The program ran to the end and printed NaN. Nothing said which entry was the problem. A silent wrong answer is the most expensive kind of bug, because it travels. Here it travelled all the way to the oven.
throw stops the function
The cure is to refuse the bad value at the place that understands it. The throw statement does that. It stops the current function at once and hands a problem upward. You throw an Error object, made with new Error(message), where the message says what went wrong.
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
console.log(readQty("12"));
console.log(readQty("1o"));
console.log(readQty("7"));
12
Error: not a number: 1o
The first call returns 12. The second throws, and since nothing catches it, Node stops the whole program with Error: not a number: 1o. The third line never runs. That is the nephew's version: loud, but it ends everything.
The pattern /^\d+$/ means "digits only, from start to end". It comes back in Module 16. For now, read it as "is this all digits?".
try and catch take the problem
A try block says "run this, and if something throws, do not stop the program". A catch block right after it says what to do instead. The thrown Error arrives in catch as a parameter, usually named err.
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
const sheet = ["12", "1o", "7"];
let total = 0;
for (const entry of sheet) {
try {
total += readQty(entry);
} catch (err) {
console.log("skipped " + entry + " (" + err.message + ")");
}
}
console.log("total " + total);
skipped 1o (not a number: 1o)
total 19
This is what Sadia wanted. The bad entry is named, with the reason. The good entries still count. Look at where the try sits: inside the loop. A throw ends only that one turn of the loop, and the next entry starts fresh. Put the same try around the whole loop, and the first bad entry would end the loop.
When nothing throws, catch is skipped completely. When something throws, the rest of the try block is skipped. In the example, total += readQty(entry) never adds anything for 1o, because readQty never returned.
The throw climbs the call stack
In real programs the throw is rarely right next to the catch. A function calls a function that calls a function, and the deepest one finds the problem. Module 5 drew this as the call stack: each call puts a frame on top, and each return takes it off.
A throw uses the same stack, from the top down. When readQty throws, its own frame ends. If its caller has no try around the call, the caller's frame ends too, and so on downward. This is called unwinding. It stops at the first frame that called inside a try. That frame's catch runs, and the program continues from there.
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
function addLine(text) {
const qty = readQty(text);
console.log("added " + qty);
return qty;
}
function importSheet(sheet) {
let sum = 0;
for (const text of sheet) sum += addLine(text);
return sum;
}
try {
console.log(importSheet(["12", "1o", "7"]));
} catch (err) {
console.log("import failed: " + err.message);
}
added 12
import failed: not a number: 1o
added 12 prints, then the second line throws inside readQty. addLine never prints, importSheet never returns a sum, and the top-level catch runs. Here one throw undid two whole functions. Whether that is right depends on the program. For Sadia's sheet it is not, and the try belongs in the loop instead. For a config file that must be all good or not used at all, it is exactly right.
The widget below replays the throw in that program, built from a real run. Each frame is pushed when its function starts. Then the throw pops them one by one. The widget plays the unwind as returns, but no frame here returns a value. Each one just ends as the error passes through.
Step through it twice. The first time, watch the stack grow to four frames. The second time, watch the unwind: nothing below readQty gets a value, and the bottom frame is where the program goes on.
An Error carries a name, a message and a stack
An Error is an object, and three of its properties matter every day. name says what kind of error it is, such as Error or TypeError. message is the sentence you passed in. stack is a text Node fills in when the Error is made: the name and message, then one line per frame, top frame first.
JavaScript throws its own Errors too. Reading a property of undefined throws a TypeError, as Module 8 showed. You catch those exactly like your own.
function firstLetter(user) {
return user.name[0];
}
try {
firstLetter({});
} catch (err) {
console.log(err.name);
console.log(err.message);
console.log(err instanceof TypeError, err instanceof Error);
}
TypeError
Cannot read properties of undefined (reading '0')
true true
{} has no name, so user.name is undefined, and reading [0] of it throws. You never wrote throw, yet the catch received a full Error. instanceof tells you its kind: a TypeError is also an Error. Lesson 02 lists every built-in kind and the real message Node gives for each.
Throw an Error, not a string
JavaScript lets you throw any value: throw "too big" is legal. Do not do it. A string has no name, no message and no stack, so the catch cannot tell what happened or where. Code that reads err.message gets undefined. The first mistake below shows it. Always throw new Error(...), or one of its kinds.
Errors at a glance
throw new Error("what went wrong"); // stop this function, hand the Error upward
try { // run this block...
risky(); // ...if it throws, jump to catch
} catch (err) { // err is the thrown Error
err.name; err.message; err.stack; // its kind, its sentence, its frames
}
// the rest of the program goes on here
The input is a list of items, each name:price. The function parseItem throws with a clear message when an item is broken. The loop catches per item, prints the good ones and counts the rest.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
function parseItem(text) {
const parts = text.split(":");
if (parts.length !== 2) throw new Error("expected name:price in " + text);
const price = Number(parts[1]);
if (!Number.isInteger(price) || price <= 0) throw new Error("bad price in " + text);
return { name: parts[0], price };
}
let refused = 0;
for (const text of tokens) {
try {
const item = parseItem(text);
console.log(item.name.padEnd(8) + String(item.price).padStart(5));
} catch (err) {
refused++;
console.log("refused: " + err.message);
}
}
console.log(refused + " refused");
tea 20
refused: bad price in cake:x2
bun 15
refused: expected name:price in samosa
refused: bad price in naan:-5
lassi 60
3 refused
Each message names the item, so a person can fix it in seconds. The try covers one item, so cake:x2 and samosa do not stop bun or lassi. Compare it with the silent NaN of the first program.
This program lets JavaScript throw a TypeError three calls deep, then prints the stack's frames. The helper keeps only lines from main.js and cuts the folder off, so the output is the same on every machine.
function cityOf(order) {
return order.customer.city.toUpperCase();
}
function label(order) {
return order.id + " to " + cityOf(order);
}
function printLabels(orders) {
for (const o of orders) console.log(label(o));
}
try {
printLabels([{ id: 7, customer: { city: "Khulna" } }, { id: 8, customer: {} }]);
} catch (err) {
console.log(err.name + ": " + err.message);
const frames = err.stack.split("\n").filter((l) => l.includes("main.js"));
for (const f of frames) console.log(f.trim().replace(/\(.*main\.js/, "(main.js"));
}
7 to KHULNA
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
at cityOf (main.js:2:30)
at label (main.js:5:30)
at printLabels (main.js:8:39)
at Object.<anonymous> (main.js:11:3)
The first label prints. Order 8 has no city, so city is undefined and calling toUpperCase on it throws. The frames read from the top: cityOf broke, label called it, printLabels called that, and the top-level code called printLabels. Each frame ends with main.js, a line number and a column. Lesson 02 reads one of these line by line.
Where this is used
- Node.js:
fs.readFileSyncthrows an Error with acodesuch asENOENTwhen the file is missing, and programs catch it to print a friendly message. - Express: an error thrown in a route handler goes to an error-handling middleware with four parameters. It sends the 500 response, and the server keeps running.
- Chrome DevTools: the Console shows every uncaught error with its stack, and each frame is a link to its line in the source.
- Sentry: it collects crash reports from apps and groups them by the error's name and its stack frames.
Common mistakes
1. A try with no catch
try {
console.log(Number("12"));
}
console.log("done");
Node refuses the file with SyntaxError: Missing catch or finally after try. A try must be followed by a catch, a finally, or both. Lesson 02 shows finally.
2. Throwing a string
function checkQty(qty) {
if (qty > 500) throw "too big";
return qty;
}
try {
checkQty(900);
} catch (err) {
console.log("problem: " + err.message);
console.log(typeof err);
}
problem: undefined
string
The catch prints problem: undefined, because a string has no message. There is no stack either, so nobody can tell where it came from. Write throw new Error("too big").
3. A name made inside try, used after it
try {
const total = Number("12") + Number("7");
} catch (err) {
console.log(err.message);
}
console.log(total);
ReferenceError: total is not defined
try is a block, so a const or let made inside it lives only inside it, as Module 6 taught. Node stops with ReferenceError: total is not defined. Declare let total = 0; before the try, then assign it inside.
Sadia runs a bakery that takes bulk orders. Her staff type each quantity into a sheet, and some of them type badly. One bad entry must not stop the whole sheet. Every entry gets its own verdict, and the good ones are added up.
Input. The first line holds n. The next n tokens are the entries, one per line.
Output. One line per entry, in order. A good entry prints ok and its value, without leading zeros. An entry made of anything but digits prints bad, the entry, a colon and not a number. An entry of digits outside 1 to 500 prints bad, the entry, a colon and out of range. The last line is total, the sum of the good values, from, their count and good.
Constraints. 1 <= n <= 1000; Every entry is 1 to 8 characters with no spaces..
Sample.
6
12
007
1e2
0
ten
500
ok 12
ok 7
bad 1e2: not a number
bad 0: out of range
bad ten: not a number
ok 500
total 519 from 3 good
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Run in Compiler
Graded as bad-quantities, a free problem in this module's problem set.
Imran is learning to read crashes. His teacher draws a chain of calls on the board: main calls the first function, which calls the next, and so on. Some functions wrap their call in try and catch, and some do not. The last function throws. Imran must say who catches the error, and how many functions it cut short on the way.
Input. The first line holds t, the number of chains. Each chain starts with a line holding n. Then n lines follow, outermost function first, each a name and either catch or pass. A function marked catch wraps its one call in try and catch. The last function always says pass and throws.
Output. One line per chain. If some function catches the error, print its name, then catches,, the number of functions that stopped early, and frames unwound. If nobody catches it, print uncaught,, then n and frames unwound.
Constraints. 1 <= t <= 100; 1 <= n <= 1000, and the n of all chains add up to at most 20000.; A name is 1 to 10 letters..
Sample.
3
4
main catch
load catch
parse pass
readQty pass
2
main pass
boom pass
3
app catch
api pass
db pass
load catches, 2 frames unwound
uncaught, 2 frames unwound
app catches, 2 frames unwound
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Run in Compiler
Graded as who-catches, a free problem in this module's problem set.
Imran writes a tiny calculator for his class. Each line is a op b. A broken line must print its error and let the others run. Three things can go wrong, and each has its own kind of Error. An operand that is not an integer throws a TypeError with not a number: and the operand. An operator other than + - * / throws an Error with unknown operator and the operator. Dividing by zero throws a RangeError with division by zero.
Input. n, then n lines, each a op b as three tokens.
Output. one line per calculation. Print the result, with / rounded toward zero by Math.trunc. For a broken line, print error, the Error's name, a colon and its message.
Constraints. 1 <= n <= 1000. Good operands are integers from -1000 to 1000.
Sample. the input below.
5
7 + 5
9 / 0
3 x 4
ten * 2
-9 / 2
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Sample output.
12
error RangeError: division by zero
error Error: unknown operator x
error TypeError: not a number: ten
-4
Run in Compiler
Common doubts
Is a throw the same as a crash?
No. A throw is a request: "someone above, please handle this". It becomes a crash only when no
catchtakes it all the way down the stack. Then Node prints the Error and its stack, and stops the program.Should I put try around everything, to be safe?
No. A
catchthat cannot do anything useful only hides the problem, which brings back the silent NaN in a new form. Puttrywhere you know what to do instead: skip the line, ask again, or show a message. Lesson 04 makes this a rule.Does catch also catch my own typos?
Yes, and that is a trap. A misspelled name throws a
ReferenceErrorlike any other Error, and a broadcatchwill report it as "bad input". Lesson 06's bug gallery shows it, with the fix: check the kind of error before handling it.What is NaN, if not an error?
NaNis a number value that means "not a valid number". Operations give it instead of throwing. That is why you check input yourself, and throw when it is wrong.
Key takeaways
throw new Error(message)stops the current function and hands the Error upward.try { } catch (err) { }takes a thrown Error, and the program goes on after thecatch.- A throw unwinds the call stack, frame by frame, until a frame that called inside a
try. - Where you put the
trydecides how much work one error undoes. - An Error has a
name, amessageand astack; throw Errors, never strings. - Go deeper: Errors Under the Hood, Completion Records, the Cost of try, and What Building an Error Really Costs (Pro).
Next: every piece of error handling, one by one, with finally, the built-in kinds and your own.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Every Piece of Error Handling, One by One: finally, the Built-in Kinds, Your Own Errors and the Stack Trace