A function handed data it cannot process has two options: return a shaky value and let the problem spread, or refuse outright. throw is the keyword of the outright refusal.
Used well, it turns a distant, baffling failure into a precise message raised at the exact spot where the bad data entered the program.
Definition
throw raises an error. The current function stops immediately, and the engine walks back up the call stack looking for the first available catch. If it finds none, the error is printed to the console and the process stops.
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.log(error.name, ":", error.message);
// Error : Division by zero
}Note the new in front of Error: you do not raise a message, you raise an object built for the occasion.
Raise an Error object, never a string
The language accepts any value at all: a number, a string, some random object. An expensive freedom, and the table below explains why.
| What is raised | What the catching block gets |
|---|---|
| A string | No call stack, no type, a bare message |
| A plain object | Useful properties, but still no stack |
| An instance of Error | Type, message, call stack and possible cause |
The call stack is the irreplaceable part: it names the failing line and the path taken to reach it.
Your own error types
As soon as an application separates several families of problems, creating your own types by inheriting from Error becomes worthwhile. Catching code can then sort with instanceof instead of comparing messages, which change on the first rewrite.
class PaymentError extends Error {
constructor(message, code) {
super(message);
this.name = "PaymentError";
this.code = code;
}
}
try {
throw new PaymentError("Card declined", "card_declined");
} catch (error) {
console.log(error instanceof Error); // true
console.log(error.name, error.code); // PaymentError card_declined
}The construction rests on class, extends and super, three keywords that are enough to obtain a complete error type. The call to super(message) is mandatory: it is what fills in the message and captures the stack.
To re-raise an error with added context without losing the original, pass it as a cause: throw new Error("Unreadable configuration", { cause: error }). The block above finds the first error again in error.cause.
Frequently asked questions
Where should you raise, and where should you catch?
Raise as close as possible to the offending data, where the program still has the context to say what is wrong. Catch as close as possible to the user, where a decision can be made: show a message, retry, fall back to a default.
Does a raised error stop the entire program?
No, it stops the current flow up to the first block able to receive it. In a browser, an uncaught error interrupts the event handler concerned but the page stays alive. In Node.js it ends the process, which is why servers wrap every request in a catching block.
Is it better to raise an error or to return null?
Returning null fits when absence is a normal result, such as a search that finds nothing. Raise an error when the situation is abnormal and no reasonable caller can continue, such as a missing API key. The criterion is not severity but this question: does the caller have anything sensible to do with that value.