Introduction

There are two main methods of handling errors, either using a Result pattern, or by throwing (and therefore handling) exceptions. The Result pattern is more common in functional programming, since functional programming avoids side-effects when possible.

The main benefit behind a Result object, or an error as a value is pretty self-explanatory. You can explicitly handle the possible error from Result, and you can see that within the type definition of the function.

This isn’t only an issue in TypeScript, but it’s the language I work with the most, so I’ll be writing the rest of this using it.

Exceptions

Whenever a function signature looks like the following, you can’t tell whether it’s going to throw or not:

// The return type from this function would just be `number`
// This isn't indicative of the actual logic unfortunately
const divide = (numerator: number, denominator: number) => {
    if (denominator === 0) {
        throw new Error("Can't divide by zero");
    }
 
    return numerator / denominator;
}

This logic gets even worse when trying to handle the errors, especially with TypeScript. This is mainly due to the function’s error not being a type, resulting in the error produced from the function being typed as unknown whenever you try to handle the error.

Additionally, if you want to define the variable outside of the try/catch, you have to create a mutable let, and it’s just a massive headache:

let x: number;
 
try {
    x = divide(5, 0);
} catch (e) {
    //   ^ the type of e is `unknown`, not helpful 😵
    console.error(e);
}
 
console.log(x);

Results

The great thing about the Result pattern is that errors are explicitly known. For example, if you had the same divide function as earlier, the return type would be something like Result<number, string>, where string is the error type.

You can create a very basic Result type in TypeScript using a union type:

export type Result<Ok, Err> =
    | { ok: true; value: Ok }
    | { ok: false; error: Err };
 
// functions to create Result easier
export const Ok = <Ok>(value: Ok) => ({ ok: true, value }) as const;
export const Err = <Err>(error: Err) => ({ ok: false, error }) as const;

The divide function, with the Result type would look something like this:

const divide = (numerator: number, denominator: number) => 
    denominator === 0
        ? Err("Can't divide by zero")
        : Ok(numerator / denominator)

Not only is this function a lot more clean (at least in my opinion, some people probably wouldn’t like the ternary operator), but it’s explicit in the possibility of an error. For example, whenever you handle the function:

const x = divide(5, 0);
 
if (!x.ok) {
    console.log(x.error);
}
 
console.log(x.value);

TypeScript makes sure you explicitly check for the ok field within the Result object before you can do anything with the function output. This is especially helpful to stop your code from randomly crashing without you even knowing that a function could possibly crash.

Summary

The Result pattern is definitely exciting, and concepts from functional programming are incredibly important. For example, Rust took heavy inspiration from many functional programming languages with their implementation of Result as a standard within their language.

Quote

“Good code is its own best documentation.” — Steve McConnell