← Blog

My take on error handling

My take on error handling

Lately I’ve been trying to get better at error handling. There are many aspects of my craft that I know I must exercise and master to feel comfortable with my work.

The first area I decided to tackle was error handling. I have to pre-face this by saying I’ve always had a problem with try/catch. It simply does not feel natural to use. It’s easy to miss. There’s no punishment or intent at the time of writing if you forget to try/catch a statement. Sometimes it’s hard to figure out whether the API we’re working with can throw or not.

## Step 1: Result types

The first step towards this, is to go back to a language I hold very dear. Rust. I love the Result and Option types in Rust, and I wanted to apply it to my current project at work. A flutter cross-platform kiosk-like application.

So the first step was to build a simple Result<T>, Success<T>, and Failure types. It looked something like this:

sealed class Result<T> {
  const Result();
  T get value => (this as Success<T>).value;

  static const success = Success<void>(null);
}

class Success<T> extends Result<T> {
  @override final T value;
  const Success(this.value);
}

class Failure<T> extends Result<T> {
  final AppError error;
  const Failure(this.error);
}

Very simple. Now all of my functions return some form of Result<T>. But there’s one problem with this approach. The libraries I depend on do not use my Result types.

Step 2: Boundaries

To get around this problem, I established a boundary. I built simple wrappers around the functions I need to use from 3rd parties that used an extension method .safe. A very simple extension method that wraps a try/catch around the function being called, and in try returns a Success<T>, and in catch returns a Failure<AppError>.

This way I can call all my 3rd party libraries, with the safety of my own code.

## Step 3: Usage

Now that everything returns a Result<T>, in order to get any sort of value to operate with, I need to access a .value property.

This forces me to first check that the Result that got returned wasn’t a Failure, with a simple line:

if (result case Failure(:final error)) doSomethingWithError(error);

And usually I will return from that. That guarantees that when I access .value I know it will be there. And my code never throws unexpectedly.

My arguments against throwing

Throwing has been blown out of proportion. In my opinion, throwing has a very specific place and time. It should only be used, when an error is truly unrecoverable, and a 3rd party library should not get to choose when it’s unrecoverable and when it is not.

Sometimes, not being able to access the database isn’t unrecoverable. If in my user-facing application, I cannot access a database that is used for logging for example, that isn’t unrecoverable. It does not affect the user experience. It should not throw and can be silently ignored.