Skip to content

Latest commit

 

History

History
93 lines (68 loc) · 4.08 KB

approach_2.md

File metadata and controls

93 lines (68 loc) · 4.08 KB

Approach 2: Throwing and Catching Errors

Throwing

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.

Catching

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.

Example

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

Guidance

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 that throw 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 the try block.

Trying it out

If you want to try this out yourself, complete the exercise in exercises/approach-2. Test your solutions by running npm run ex-2.