Handling Errors with try, catch and finally

try runs code that might fail, catch takes over when it does and gets an error object with a name and a message, and finally runs either way, so one bad value doesn't stop your whole program.

Apprentice · JavaScript · Software Development · Updated

After this lesson you can

  • Catch an error with try and catch, and read its name and message
  • Use finally for code that must run whether or not an error happened

Key takeaways

  • An error that nothing catches stops the program, and every line after it is skipped.
  • Put the code that might fail in a try block. When it throws, JavaScript jumps to the catch block and the program carries on.
  • The catch block gets an error object. Its name says what kind of problem it was, and its message says what went wrong.
  • A finally block runs whether the try block worked or failed, so clean-up code goes there.
  • A catch should do something useful, such as showing a message or using a fallback, never hide the error in silence.

You've seen error messages since your first program: a typo, and Node stops with a red line of text. In a real app, some errors come from outside your code: a saved file that got damaged, a person who typed "ten" where a number goes, a website that didn't answer. A shopping app that hits one of those shouldn't go blank. It should say Something went wrong, please try again and keep working. This lesson shows how your code can expect a problem, catch it and carry on.

This chapter's running example is a recipe scaler: a small program that takes a recipe card, such as Saturday Pancakes (it serves 4), and works out the amounts for 10 people, in cups or in milliliters.

When nothing catches an error

The scaler keeps its recipe cards as JSON text, the format you met in JSON: Data You Can Save and Send. JSON.parse turns that text back into an object. This card got cut off at the end: its closing } is missing.

const savedRecipe = '{ "name": "Saturday Pancakes", "servings": 4 ';
const recipe = JSON.parse(savedRecipe);

console.log(`Making ${recipe.name}`);
console.log("Time to cook!");

JSON.parse can't read it, so it throws an error: it stops and reports the problem. Nothing in this program catches the error, so Node prints it and stops the whole program. Neither console.log line runs. Above the error, Node shows the text it couldn't read; then comes the error itself:

SyntaxError: Expected ',' or '}' after property value in JSON at position 45 (line 1 column 46)
    at JSON.parse (<anonymous>)

The lines below these give the file and the line number, 2, where JSON.parse was called, as in Your First JavaScript Program. An error like this, one that happens while the program is running, is called a runtime error.

try and catch: a safety net

Think of a trapeze act at the circus. The performer tries the trick, and if they slip, the net catches them and the show goes on. JavaScript's net has two parts:

  • try (read "try") holds the code that might fail.
  • catch (read "catch") holds the code to run if it does.
const savedRecipe = '{ "name": "Saturday Pancakes", "servings": 4 ';

try {
  const recipe = JSON.parse(savedRecipe);
  console.log(`Making ${recipe.name}`);
} catch (error) {
  console.log("That saved recipe is damaged, so here's a fresh card.");
}

console.log("Time to cook!");
That saved recipe is damaged, so here's a fresh card.
Time to cook!

When JSON.parse throws, JavaScript skips the rest of the try block (the Making line never runs) and jumps straight into catch. After the catch block, the program carries on as normal, so Time to cook! prints. When nothing goes wrong, the catch block is skipped. The whole thing, try and catch together, is called a try...catch statement.

Try it. Make a file named damaged-card.js with the code above and run it with node damaged-card.js. Then add the missing } before the last ' on the first line and run it again. Which lines print now?

The error object: name and message

The word in parentheses after catch, here error, is a variable you name. JavaScript puts an error object in it: an object that describes what went wrong. Two of its properties do most of the work:

  • name: what kind of error it is, such as TypeError.
  • message: what went wrong, in words.
const recipe = { name: "Saturday Pancakes", servings: 4 };

try {
  console.log(recipe.ingredients.length);
} catch (error) {
  console.log(`name: ${error.name}`);
  console.log(`message: ${error.message}`);
}
name: TypeError
message: Cannot read properties of undefined (reading 'length')

This card has no ingredients, so recipe.ingredients is undefined, and undefined has no length. These are the error names you'll meet most:

Name What it usually means An example that causes it
SyntaxError Text that isn't valid code or valid JSON JSON.parse("four")
TypeError A value used in a way its type can't do undefined.length, or calling something that isn't a function
ReferenceError A name that doesn't exist servingz * 2 when the variable is servings
RangeError A number outside the range allowed (2.5).toFixed(200): toFixed allows 0 to 100 digits

The stack: where it happened

An error object has one more useful property, stack: the error's name and message, then a list of where the program was when it happened, newest first. It's called a stack trace, because it lists the stack of function calls that led to the error (the call stack). When this program runs console.log(error.stack) in its catch block:

function countIngredients(recipe) {
  return recipe.ingredients.length;
}

try {
  countIngredients({ name: "Saturday Pancakes" });
} catch (error) {
  console.log(error.stack);
}

the first lines it prints are these (on your computer the folder part of the path is your own, shown here as …):

TypeError: Cannot read properties of undefined (reading 'length')
    at countIngredients (…/stack.js:2:29)
    at Object.<anonymous> (…/stack.js:6:3)

Read it from the top: the error happened in countIngredients, on line 2, character 29, and countIngredients was called from line 6. Object.<anonymous> is Node's name for the top level of the file, the code that isn't inside any function. When an error surprises you, the top line of the stack trace that's in your file is where to look first.

finally: it runs either way

After you cook, you put the recipe card back in the box, whether the pancakes turned out or not. That's finally (read "finally"): a third block that runs after try and catch, every time, whether there was an error or not.

function readRecipeCard(text) {
  console.log("Taking the card out of the recipe box...");
  try {
    const recipe = JSON.parse(text);
    console.log(`Making ${recipe.name} for ${recipe.servings}.`);
  } catch (error) {
    console.log(`Can't read this card: ${error.message}`);
  } finally {
    console.log("Card back in the box.");
  }
}

readRecipeCard('{ "name": "Banana Bread", "servings": 8 }');
readRecipeCard('{ "name": "Banana Bread", "servings": ');
Taking the card out of the recipe box...
Making Banana Bread for 8.
Card back in the box.
Taking the card out of the recipe box...
Can't read this card: Unexpected end of JSON input
Card back in the box.

The first card works, so catch is skipped; the second throws, so catch runs. Both times, finally runs. It even runs when the try block ends with a return. On a web page, finally is where a Loading... message gets hidden, so it disappears whether the data arrived or not.

A catch should do something useful

A catch block that's empty, or that only says "oops", hides the problem: the program carries on with bad data, and nobody knows why. A useful catch does something a person can work with: it shows a clear message, or it uses a sensible fallback and says so.

function loadServings(text) {
  try {
    return JSON.parse(text).servings;
  } catch (error) {
    console.log(`Couldn't read the saved servings (${error.message}). Using 4.`);
    return 4;
  }
}

console.log(loadServings('{ "servings": 6 }'));
console.log(loadServings("six"));
6
Couldn't read the saved servings (Unexpected token 's', "six" is not valid JSON). Using 4.
4

Keep the try block small, too: wrap the line that can fail, not the whole program. Then a catch knows what went wrong.

Try it. In damaged-card.js, wrap the reading of the card in a function like loadServings, and call it with three texts: a good card, a card cut off halfway, and an empty string "". Print error.name and error.message in the catch block. Which error does the empty string give?

Check yourself

2 questions to try on the lesson page.

So what

Every app that reads files, forms or websites meets bad data sooner or later. With try, catch and finally, your program notices, says what went wrong, cleans up and keeps going, instead of stopping in front of the person using it.

Key terms

Cheat sheets

Related