newsletter

Error handling is key: here is how to do it (well)

Errors are a key component of any program. Let's explore multiple strategies for error handling, from throw/catch to the Either type and Monads.


Sandro Maglione

Sandro Maglione

Software development

Programs are full of errors. This is expected 💁🏼‍♂️

As such, languages offer tools to deal with errors, aka error handling.

The community is starting to see how errors are a key part of software development, not an afterthought.

Let's learn some patterns to deal with errors 👇


An issue of throwing and catching

Most languages went down the path of throw/catch.

The concept sounds good and simple (in theory):

  • When something goes wrong use throw, it will stop everything and crash
  • When you want to handle (any) error use catch to prevent crashing and do something else

It gets as concise as it can be:

const mightError = () => {
  if (Math.random() > 0.5) {
    throw new Error();
  }

  return 'success';
};

/// 👇 Somewhere else up the chain
const main = () => {
  try {
    // Do something, regardless of possible errors
  } catch (e) {
    // Handle (any) error
  }
};

It gets bad, fast

Now, what's the problem with this?

2 core issues:

  1. Handling multiple errors becomes a nightmare
  2. The error type inside catch is unknown

If we want to handle errors in different ways we get 2 options:

  1. throw custom error instances, and use instanceof inside catch
class MyError extends Error {}

const mightError = () => {
  if (Math.random() > 0.5) {
    throw new MyError();
  }

  return 'success';
};

/// 👇 Somewhere else up the chain
const main = () => {
  try {
    // Do something, regardless of possible errors
  } catch (e) {
    if (e instanceof MyError) {
      // throw of custom error (somewhere 💁🏼‍♂️)
    }

    throw e; // 👈 throw again other errors
  }
};
  1. Use multiple try/catch
const mightError1 = () => //
const mightError2 = () => //

/// 👇 Somewhere else up the chain
const main = () => {
  try {
    mightError1();
  } catch (e) {
    // Handle errors (part 1)
  }

  try {
    mightError2();
  } catch (e) {
    // Handle errors (part 2)
  }

  // And more if needed 😐
};

No surprise that devs avoid this all together. It's not fun, not practical, and it gets complex, fast.

This bad impression justifies the push-back of some devs on the topic of error handling: "Better not to deal with this" 😶

Better not to throw

Some people realized that throw is "bad", and that maybe we can treat errors as values.

Simple solution is returning 2 values: error or data.

const mightError = () => {
  if (Math.random() > 0.5) {
    return { error: new Error() };
  }

  return { data: 'success' };
};

const main = () => {
  const { data, error } = mightError();

  if (error !== undefined) {
    // We got an error, do something!
  }
};

You get many variations of this pattern, using arrays, objects, union types; anything that can contain "2 things":

It gets ugly, fast

Some improvements compared to throw, but the experience is still "cranky".

if becomes an integral part of the error handling strategy, and it looks bad:

const mightError1 = () => //
const mightError2 = () => //

const main = () => {
  const { data: data1, error: error1 } = mightError1();

  if (error1 !== undefined) {
    // We got an error, do something!
  }

  const { data: data2, error: error2 } = mightError2();

  if (error2 !== undefined) {
    // We got an error, do something!
  }

  // And more if needed 😐
};

Each function that "might error" needs a new if to check for errors and stop the program flow. Not good.

There is even a proposal to add a new ?= syntax to make this even more common 👇

const [error, response] ?= await fetch("https://arthur.place");

Not good if you ask me 👀

Result type someone?

There is another solution. It comes from the obscure world of functional programming, it's called Either (Result) type.

It's something that can have either an error or a success value:

class Left<L, R> {
  readonly _tag: 'Left' = 'Left';

  constructor(readonly left: L) {}
}

class Right<L, R> {
  readonly _tag: 'Right' = 'Right';

  constructor(readonly right: R) {}
}

export type Either<R, L> = Left<L, R> | Right<L, R>;

const mightError = (): Either<string, Error> => {
  if (Math.random() > 0.5) {
    return new Left(new Error());
  }

  return new Right('success');
};

Nice nice. Not enough.

This has the same issue as before, we need to check for errors at each step:

const main = () => {
  const value1 = mightError1();

  if (value1._tag === "Left") {
    // We got an error, do something!
  }

  const value2 = mightError2();

  if (value2._tag === "Left") {
    // We got an error, do something!
  }

  // And more if needed 😐
};

Welcome to monads (that's right)

We need something that:

  • Collects errors as values
  • Let's us move on as if errors are not there
  • Allows to handle all the collected errors (all together or one by one)

Solution (again) from functional programming. It's (technically) called monad.

In practice you see it in the wild as flatMap/andThen/bind.

That's how it looks like in Effect 👇

Generators (yield*) collect possible errors at each step, while at the same time returning the success value for us to use.

Only at the end you can extract the return value similar to Either (error or success).

For a full breakdown of the core issue with error handling in typescript check out this video 👈


My effect course was published last week. Definitely a positive response 💯

I've just finished reading it. 🚀 - Super intuitive example (API fetch) - Well-structured: The organization of topics made it easy to follow along. - I like the "let's refactor this" parts - You won't get far without coding while you read: This hands-on approach is crucial for…

Sandro Maglione
Sandro Maglione
@SandroMaglione

@EffectTS_ Beginners Complete Getting Started course is out now 🚀 From zero to building complete apps with effect Type-safe, maintainable, testable apps, and it's just typescript 💯 Here is what you'll learn 👇🧵 typeonce.dev/course/effect-…

7
Reply

Meanwhile I am working on the next in the pipeline: XState course.

Stay tuned, I am coming strong into Q4 this end of 2024 🔜

See you next 👋

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.