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:
-
This function isn’t “honest.” In fact, nothing in its signature
(a: number, b: number) => numberindicates that it can throw an exception—that is, produce a side effect. You have to read the implementation to find out… thanks, TypeScript 😉 -
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
aandb, and encapsulate the primitives. Ifbis no longer of typenumberbutDenominator, it would likely be up to that type to prevent the creation of a zero denominator. -
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:
- In the case of passing by value (
Ok), performing further processing based on the result of the division; - In the case where the check fails (
Ko), propagate the error, since there is nothing else to do.
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:
Ok-> code 200Ko-> code 422 (Unprocessable Content)
“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!