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.