Error Handling — No Exceptions!

Callum Linington
6 min readMar 22, 2023

--

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.

Record Type

You can view this as a type that holds both every value of string with every value of int

Or Type

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:

Bad GetById Method

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.

Encoded Errors

Each type simply encodes each of these experiences. Now this is done, we can modify our original method to use OneOf instead.

First pass

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.

Domain Errors Base

Here OneOfBase<> provides us with the ability to just derive a single type to represent a discriminated union.

Previous Method enchanced

Now we got that out of the way, let’s handle this!

Updated weather handler

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 .

Error handler

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

Database Save Method
Validation Additions

Here we’ve created a new discriminated union ValidationErrors which is part of the generic parameters in DomainErrors .

Handle validation errors

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.

The Chain

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.

--

--