Error Handling — No Exceptions!
There are times when exceptional circumstances hit your application — it’s ran out of memory, accessing that bit of memory went wrong. More often than not, we just want to encode some errors in our application — user gave a short password, there weren’t as many rows of data as we were expecting, we couldn’t find that particular record.
In this article, I just want to look at how we can effectively handle our errors without using exceptions — because they aren’t exceptional circumstances.
OneOf
Let’s get straight to the point, we’re going to be using the OneOf library. OneOf is an encoding of Discriminated Unions in C#.
The easiest way to think about a discriminated union is an or type. This is opposed to a sum type.
You can view this as a type that holds both every value of string with every value of int
The above image shows a C# type that is now an or type. So this can hold every value of string or every value of int — but not both.
But there’s very few times where you’ll be just switching primitives. It’s more likely that we want to switch types — when? When we’re handling errors.
Defining Errors
Let’s take this very simple example:
As we discussed at the top of the article, non of these are truly exceptional circumstances — in fact they are very expected, seeing as we’re testing for them and throwing exceptions.
But at the moment, we’re focussing on what the errors are. It looks like there is a potential for not providing an id value. The id value could be in the wrong format. Finally it looks like there could be a case where the database couldn’t find request weather forecast.
Lets encode these as types.
Each type simply encodes each of these experiences. Now this is done, we can modify our original method to use OneOf instead.
Not a huge amount has changed here on the face of it. Firstly, we’re not throwing any more exceptions so this can be a performance improvement. Secondly, it’s easier to understand the scenarios of failure because they’re written out in an understandable way. Finally, and I think the biggest part of this is that our signature does not lie.
Signature does not lie
I heard this phrase I think during a Scott Wlaschin talk and it just has stuck with me ever since. In C# exceptions are very secretive, illusive, hidden. They also come at a performance and developer experience cost. There’s nothing really shouting out at us that there could be potential problems in running this code — with exceptions, the best you could do is put xml documentation. The signature is not giving you all the information. The developer experience for exceptions is also just painful, you have to wrap chunks of code in try statements, and the continuity of function execution is interrupted. Should you even have to worry about the error path? Couldn’t it just exist without messing up your code comprehension?
Sound, Discriminated Union, Action
Now that we have established that doing this could have some positive impacts on code, lets have a look at that.
Quickly though, I just don’t want to keep outlining that humongous type. Lets shorten it.
Here OneOfBase<>
provides us with the ability to just derive a single type to represent a discriminated union.
Now we got that out of the way, let’s handle this!
This just looks beautiful IMHO. The GetById
method has returned a result and we have handled every possible case quite explicitly. Hovering the mouse over the method will show that signature. Now lets take a quick look at ErrorHandler.Handle
.
Here we can handle all the errors that exist in DomainErrors
. This isn’t in the place where it happens because it doesn’t have to be. I don’t want to keep seeing how the error path is handled — just happy that it is — if we add another domain error, then the compiler helps us out and tells us to implement it’s return. One enhancement here is that we could pass a specific message into our errors so that they can just be surfaced here.
If you’ve noticed as well, we could refactor the weather handler to convert the match anonymous lambdas to method groups. Match<IResult>( TypedResults.Ok, ErrorHandler.Handle )
.
Chaining
Here we’ve created a new discriminated union ValidationErrors
which is part of the generic parameters in DomainErrors
.
I fully expect this method to keep on growing with all parts of our application errors ending up here — there’s nothing to say that you couldn’t have each application behaviour’s errors handled in a class that exists under that behaviour — then bring these handlers together in a root ErrorHandler
.
I’ve also demonstrated a slightly different way to handle returning back to one result — if they all basically mean the same thing.
Next, lets look at the impact on our API surface.
Above is the chaining, so all those quick additions led us to this moment.
Take a second to take in the fact that our error path is pretty much hidden. Our intentions are very clear — Validate => Save
. We can just chain match after match letting our DomainErrors
fall through.
It would be so amazing if this became an actual C# type because then the switch
expression would make this so much cleaner and nicer to use.
Conclusion
This is not the end, we’ve only just begun. We’re only using the OneOf
library here — there’s actually a better type to represent the success flow and error flow: Either<TLeft, TRight>
. We’ll look at the next time, because we’ve learnt enough in this one post — everything so far is probably very new to you and wrapping your head around it will take some time.
I hope that you’ve seen the power of using discriminated unions — mainly in bringing clarity into our code. Clarity in the success path, clarity in the error handling, clarity in software correctness and clarity in developer experience.
Coming up soon will be using Discriminated Unions in Event Sourcing, also looking at how we can make use of Language Extensions library to improve code flow and fitment.