Learn JavaScript

Lesson 1 of 9 ¡ Error Handling: throw, try, catch, finally, and Reading a Stack Trace

Module 10 ¡ Error Handling: throw, try, catch, finally, and Reading a Stack Trace

Errors, the Picture: throw, catch, and a Call Stack That Unwinds

FreeReading

In this lesson

  • Throw an Error from a function, and catch it with try and catch, so one bad input does not end the whole program.
  • Picture a throw climbing the call stack, frame by frame, until a catch takes 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
Run in Compiler

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
Run in Compiler

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
Run in Compiler

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
Run in Compiler

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.

The same steps as a table, one row per step.

StepLabelDetailReturnsWhat happened
1top-level codetry { importSheet(sheet) }: its catch takes Error: not a number: 1o
2importSheet(sheet)12 was added; now calls addLine("1o")nothing: the Error passes through
3addLine(text)text = "1o", calls readQty(text)nothing: the Error passes through
4readQty(text)text = "1o", not all digitsnothing: it throws Error: not a number: 1o

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
Run in Compiler

{} 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
Example 1: a price list that names every bad item

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.

Run in Compiler
Example 2: where did it break?

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.

Run in Compiler

Where this is used

  • Node.js: fs.readFileSync throws an Error with a code such as ENOENT when 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.

Brain teaser

Which lines print?

Zara likes to predict before she runs. Here a loop of three calls sits inside one try.

function check(n) {
  console.log("check " + n);
  if (n > 2) throw new Error("too big: " + n);
  console.log("ok " + n);
}
try {
  check(1);
  check(3);
  check(2);
} catch (err) {
  console.log(err.message);
}
console.log("done");

How many lines print, and does check 2 appear?

Exercise 1Easy

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.

Exercise 2Easy

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.

Exercise 3Hard

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 catch takes 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 catch that cannot do anything useful only hides the problem, which brings back the silent NaN in a new form. Put try where 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 ReferenceError like any other Error, and a broad catch will 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?

    NaN is 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 the catch.
  • A throw unwinds the call stack, frame by frame, until a frame that called inside a try.
  • Where you put the try decides how much work one error undoes.
  • An Error has a name, a message and a stack; 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