Throwing Your Own Errors

When a function gets a value it can't use, throw new Error with a message that says what was wrong, make your own error class with extends Error for the problems your program knows about, and return a value instead when there's no mistake to report.

Journeyman · JavaScript · Software Development · Updated

After this lesson you can

  • Throw an Error with a clear message when a function gets a value it can't use
  • Make a custom error class with extends Error and tell it apart with instanceof
  • Choose between throwing an error and returning a value

Key takeaways

  • throw new Error("...") stops the function at once and hands the error to the nearest catch.
  • A helpful message says what was wrong, shows the value that came in and says what would work.
  • A class such as ServingsError extends Error gives your errors their own name. instanceof tells them apart in a catch block.
  • A catch block that only handles one kind of error throws the others on with throw error.
  • Return a value for a normal answer, even "nothing found". Throw when the caller made a mistake or the job can't be done.

Every function you write gets used by someone: other parts of your program, a teammate, or you in six months. Sooner or later one of them calls it with a value that makes no sense, like scaling a recipe to -2 servings. JavaScript won't always complain. Sometimes it hands back a wrong answer, and the bug shows up three steps later, far from where it started. In this lesson your functions stop bad values at the door with a clear message: the same thing JSON.parse does when it gets text that isn't JSON.

The problem: wrong answers, no complaints

Here's the recipe scaler's core: divide by the servings the card makes, multiply by the servings you want.

function scaleAmount(amount, fromServings, toServings) {
  return (amount / fromServings) * toServings;
}

console.log(scaleAmount(2, 4, 10));
console.log(scaleAmount(2, 4, -2));
console.log(scaleAmount(2, 4, "ten"));
5
-1
NaN

2 cups of flour for 4 becomes 5 cups for 10: right. But -1 cups and NaN cups (NaN is read "not a number", from Types and Converting Between Them) are nonsense, and the program carries on as if they were fine. A shopping list would print Buy NaN cups of flour.

throw: refuse the value, out loud

A good restaurant kitchen doesn't quietly cook a wrong order. It sends the ticket back: We can't make this, we're out of eggs. Your function can do the same with throw (read "throw"): it stops the function at once and hands an error to the nearest catch.

You make the error with new Error("message"), the same kind of error object JSON.parse throws. Together, throw new Error("...") is read "throw new error".

function checkServings(servings) {
  if (!Number.isInteger(servings) || servings <= 0) {
    throw new Error(`Servings must be a whole number above 0, but got ${servings}.`);
  }
  return servings;
}

function scaleAmount(amount, fromServings, toServings) {
  checkServings(fromServings);
  checkServings(toServings);
  return (amount / fromServings) * toServings;
}

try {
  console.log(scaleAmount(2, 4, 10));
  console.log(scaleAmount(2, 4, -2));
  console.log("This line is skipped.");
} catch (error) {
  console.log(error.message);
}
5
Servings must be a whole number above 0, but got -2.

Number.isInteger(servings) is true only for a whole number: 10 yes, 2.5 no, "ten" no. The ! (read "not") flips it, and || (read "or") joins the two checks, as in Combining Conditions with And, Or and Not. When throw runs, nothing after it in checkServings runs, nothing after the call in scaleAmount runs, and the line that was waiting for the answer is skipped too. JavaScript jumps to the catch.

A message that helps

The message is for a person: the one who reads it in the Console, or sees it on the page. A helpful message answers three questions: what was wrong, what value came in, and what would work.

Not much help Helpful
Error Servings must be a whole number above 0, but got -2.
Invalid input Unknown unit "pinch". Use cups, tbsp, tsp or ml.
Something went wrong The recipe card has no ingredients list.

Try it. Make scale.js with the code above. Call scaleAmount with "ten", then 2.5, then 0, each in its own try. Does every message tell you what to fix?

Your own kind of error

Each error object has a name, and so far yours all say Error. When a program has its own kinds of problems, it can give them their own names, the way a car's dashboard has a fuel light and an oil light instead of one light that says problem. You make a new kind of error with a class that extends Error, as in Inheritance with extends and super:

class ServingsError extends Error {
  constructor(message) {
    super(message);
    this.name = "ServingsError";
  }
}

function checkServings(servings) {
  if (!Number.isInteger(servings) || servings <= 0) {
    throw new ServingsError(`Servings must be a whole number above 0, but got ${servings}.`);
  }
  return servings;
}

try {
  checkServings("ten");
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
  console.log(error instanceof ServingsError);
  console.log(error instanceof Error);
}
ServingsError: Servings must be a whole number above 0, but got ten.
true
true

super(message) runs Error's own constructor, which keeps the message (and the stack). this.name gives the error its name. This is called a custom error. The word instanceof (read "instance of") asks whether an object was made from a class, or from a class that extends it. A ServingsError is an Error too, so both lines print true.

Catch the errors you can handle

With its own name, a catch block can tell a ServingsError (the person typed something wrong: ask again) from any other error (a bug: let it through). This part of a catch block handles one kind and throws the rest on with throw error, so a bug is never hidden:

} catch (error) {
  if (error instanceof ServingsError) {
    console.log(`Please pick the servings again. ${error.message}`);
  } else {
    throw error;
  }
}

The else throws the same error again, so it travels on to the next catch further out, or stops the program with its stack trace, where you'll see it and fix it.

Throw, or return a value?

Not every "no" is an error. Ask the pantry for saffron and it says "we don't have any": that's a normal answer, not a mistake. Here find (from Finding Things in an Array) returns undefined when nothing matches, and the caller checks for it:

const pantry = ["flour", "sugar", "eggs", "butter"];

function findIngredient(name) {
  return pantry.find((item) => item === name);
}

console.log(findIngredient("sugar"));
console.log(findIngredient("saffron"));
sugar
undefined

A rule of thumb:

The situation What to do Example
A normal answer, even "nothing" Return a value: undefined, null, false or an empty list [] No recipe matches a search
The caller made a mistake Throw -2 servings, a unit called "pinch"
The job can't be done at all Throw A recipe card with no ingredients

When you're not sure, ask: should the caller have known better? If yes, throw. If the answer is a normal part of the job, return it.

Check yourself

3 questions to try on the lesson page.

So what

The code you write gets called with surprises. A function that throws a clear error at the first bad value turns a mystery three steps later into one sentence that says what to fix, and that's what every good library you'll use, from JSON.parse on, does for you.

Key terms

Cheat sheets

Related