During runtime, errors can be thrown in our application unexpectedly by computations acting on faulty computations produced earlier (like the first example above). We can also manually throw errors ourselves by using the throw
keyword. This will immediately terminate the application, unless there is a catch
block in the call stack.
Errors that have been thrown can be caught using a try...catch
block. The catch
block will catch all errors that arise in the try
block, even if they are programmer errors. Ideally there would be sufficient logic in the catch
block to differentiate these cases so that we are not at risk of recovering from a programmer error as though it is an operational error.
const applyToInteger = (func, integer) => {
if (typeof func !== "function") {
throw new TypeError("Invalid argument: First argument is not a function");
}
if (!Number.isInteger(integer)) {
throw new TypeError(`Invalid argument: Second argument ${integer} is not an integer`);
}
return func(integer);
};
Using this function in the REPL:
> applyToInteger((n) => 2 * n, 2)
4
> applyToInteger((n) => `You passed ${n}`, -4)
'You passed -4'
> applyToInteger({}, 2)
TypeError: Invalid argument: First argument is not a function
...
> applyToInteger((n) => n, 2.3)
TypeError: Invalid argument: Second argument 2.3 is not an integer
...
If we wish to be able to recover, we can augment this approach by using a try/catch
block. The try block tries to execute the code. If no error is thrown during the try block, the catch block will not run. However if the try block throws an error, the catch block will catch the error and do something with it.
const applyAndPrintResult = (func, integer) => {
try {
const result = applyToInteger(func, integer);
console.log("Result successfully calculated:");
console.log(`Applying ${func.name} to ${integer} gives ${result}`);
} catch (e) {
console.log("Sorry, result could not be calculated:");
console.log(e.message);
}
};
Using this function in the REPL:
> applyAndPrintResult(function double (n) { return 2 * n; }, 2)
Result successfully calculated:
Applying double to 2 gives 4
> applyAndPrintResult(function increment (n) { return n + 1; }, -4)
Result successfully calculated:
Applying increment to -4 gives -3
> applyAndPrintResult({}, 2)
Sorry, result could not be calculated:
Invalid argument: First argument is not a function
> applyAndPrintResult((n) => n, 2.3)
Sorry, result could not be calculated:
Invalid argument: Second argument 2.3 is not an integer
While fairly drastic, throwing errors is a useful approach and is appropriate in many cases.
- Throwing can be useful for making critical assertions about the state of your application, especially during startup (e.g. database connection has been established).
- It's not possible to wrap an asynchronous function in a try/catch block, so throwing should only be used with synchronous code. Errors thrown from asynchronous functions will not be caught. To understand why, learn about the javascript call stack.
- Remember to use
catch
blocks to avoid inappropriate program termination. (e.g. a server should usually not crash in the course of dealing with a client request). - Without
catch
blocks codebases thatthrow
errors extensively will be very fragile. - Do not simply log the error in a
catch
block. This can be worse than no error handling at all. - Note that
catch
will trap errors that are thrown at any point in the call stack generated by thetry
block.
If you want to try this out yourself, complete the exercise in exercises/approach-2. Test your solutions by running npm run ex-2
.