Mathieu Eveillard

You’re using exceptions incorrectly

You’re using exceptions incorrectly

You might be tempted to write this:

const divide = (a: number, b: number): number => {
  if (b === 0) {
    throw Error("Division by 0.");
  }
  return a / b;
};

Although simple, this function is very problematic. I see at least three issues:

  1. This function isn’t “honest.” In fact, nothing in its signature (a: number, b: number) => number indicates that it can throw an exception—that is, produce a side effect. You have to read the implementation to find out… thanks, TypeScript 😉

  2. On the other hand, if this function is not just a pure utility function outside of any business context, we might want to give more meaning to the parameters a and b, and encapsulate the primitives. If b is no longer of type number but Denominator, it would likely be up to that type to prevent the creation of a zero denominator.

  3. But, more fundamentally, error handling poses a semantic problem. The edge case of division by 0 is entirely predictable. You therefore have no reason to throw an exception to handle it—a mechanism that is supposed to remain… exceptional. Yes, throwing an exception means acknowledging that your application is in a state of failure (PLS) and can no longer handle the situation.

In other words, you want to communicate with the caller—possibly the end user—so that they can adjust their behavior. Whereas with an exception, you’re talking to the developers, telling them there’s a bug in the application and that they need to fix it as soon as possible, please!

So you’ll need to find another way to handle this type of error.

What we want is a type—let’s call it Result—that allows us to represent both the nominal case, where division can take place (Ok), and the boundary case where division cannot be performed because b is zero (Ko).

Furthermore, we want this type to be useful by exposing a bind function that will allow us to:

This is what is called polymorphism: the implementations ok() and ko() both satisfy the contract defined by the Result type, but each in its own way, since in one case we have a value, and in the other, an error.

Gosh, this is interesting!

In fact, we can now chain instructions and write divide(1, 2).bind(square) as well as divide(1, 0).bind(square). Any errors no longer cause the program to fail; the function divide: (a: number, b: number) => Result<number, DivisionByZeroError> clearly states what it does, and everyone is happy.

All that’s left is to add a finally method to produce side effects at the end of the chain, which, in an HTTP controller, for example, would look like this:

“What about asynchronous operations?” you might ask. This case requires a bit more work, but the idea remains simple: compose Result and Promise to manipulate a Promise<Result> type and no longer use Promise.reject, since Promise.resolve manipulates a Result type that already represents the passing and failing cases.

You can add these few lines of code to your codebase; they have the merit of being simple and allow everyone to understand exactly what’s happening. Or you can use a code library—the go-to choice in the TypeScript world being Neverthrow.

In any case, congratulations—you’ve created your first monad!

← All posts