Yuan

Explicit Error Handling in TypeScript without Either monoid

Here’s an example of error handling done with try catch:

function doStuff(): Foo {
  // ...
  if (condition) {
    throw new CustomError("desc");
  }
  // ...
  return { foo: "bar" };
}

function main() {
  try {
    // ...
    const foo = doStuff();
    // do something with foo
  } catch (e) {
    // handle the error
  }
}

Here’s another way of doing it in a more functional approach:

import { left, right, Either, isLeft, isRight } from "...";

function doStuff(): Either<Foo, CustomError> {
  // ...
  if (condition) {
    return left(new CustomError("desc"));
  }
  // ...
  return right({ foo: "bar" });
}

function main() {
  // ...
  const foo = doStuff();
  if (isLeft(foo)) {
    const e = foo.left;
    // handle the error
  }
  const val = foo.right;
  // do something with foo
}

There are a lot of great articles out there such as [1] and [2] describing the pros and cons of each paradigms so we won’t go over them here.

So what is wrong?

The second approach feels overly verbose and foreign to JavaScript and TypeScript developers. Some people find it harder to reason about.

Alternative approach

It is important to remember that JavaScript is a dynamically typed language. We could accomplish the same thing without the additional burden:

function doStuff(): Foo | CustomError {
  // ...
  if (condition) {
    return new CustomError("desc");
  }
  // ...
  return { foo: "bar" };
}

function main() {
  // ...
  const foo = doStuff();
  if (foo instanceof Error) {
    // foo's type is inferred to be `CustomError`
  }
  // foo's type is inferred to be `Foo`
}

This is in my opinion more elegant and clear in intent that its monoid counterpart. I have convinced my team at my previous company to adopt this pattern and it is currently working great and running in production.

Arbitary number of error types is still supported given that every custom error type inherits from Error.

It is worth noting that this approach is heavily depends on TypeScript as the compiler will force you to handle the error. A developer might forget to check for error and let it pass silently in pure JavaScript.