How to Avoid Exceptions

Callum Linington
12 min readFeb 28, 2024

--

If you’ve been following along you’ll know I have a beef with Exceptions. If you haven’t been following along, I have a beef with Exceptions. This will be the final chapter in showing how I kick Exceptions to the curb and introduce Either<> (Or the Result type in other langs) to completely (but not completely) solve the problem with Exceptions.

Just to make you aware (in case you haven’t been following my stories):

  1. Error Handling — No Exceptions :: This is the start of the saga, championing Discriminated Unions
  2. Stop using Exceptions :: This is my throw down with Exceptions and sets the scene for this resolution story
  3. The Value of Value Objects :: This isn’t absolutely necessary, but would be a great help

Abstract

Setting the scene, I would definitely give the first story a good read, it’s listed above. The long and short of it is, Exceptions introduce a lot of problems, they make the code a mess, they’re unenforceable, they creep up on you and destroy your life. In this article, I can show you how to abstract them away, abstract away the error handling altogether and introduce a highly extensible, highly maintainable and robust solution.

Either

Lets start with the either type. In the first post I showcased the discriminated union or sum type (if you want to get all official), this can be simply described as if we have a Car type and some sub-types like Mustang , Chevvy , Ferrari or Lambo wherever a variable is declared as a Car it can only be one of those sub types. Kinda like inheritance in OOP.

Sum Type (Discriminated Union)

Above the let is the codelens showing its type Car. The key thing with this type is its value can only be one of those types defined inside Car . To use the variable you have to match against it and handle all cases:

I used F# here in these examples because it’s super terse, it’s quite easy to understand the code when there’s only useful tokens there.

But, this will be in C# so here you go:

It’s actually not that bad, the primary constructor syntax here has really reduced the code beautifully. You’ll notice the car.Match , you could use the switch expression in C# but it always requires a _ => case which kinda sucks. And the OneOf library adds this method with all the cases so acts more like F# where you have to handle every case (and only your cases, not some fantom _).

Okay this is cool, but what has it got to do with Either , well, Either is just a fixed size Discriminated Union, there are only two paths Left or Right .

This is what it could look like — however, we want to be using a fully featured Either , so we’ll be using LanguageExt and I’ll show you a small example first before we go neck deep in this ocean.

Starting at the top, we have 3 domain types created. Apple which is representing the Right(good path). Orange which is representing the Left(bad path). EatenApple represents a change made to the domain type Apple (think about the difference between, FreeUser and PaidUser , it shows a progression in the domain from free user to paid when a fee is settled — N.B. it could just be a boolean on User but this can lead to unrepresentable state being represented if we have too many boolean flags)

Next we execute GetFruit , if a random number (never copy this example of creating a random number >.< ) is above 5 then we get the bad fruit — but if the number is 5 and below we get the good fruit, an Apple . Looking at Map (a Select equivalent) it takes a parameter Func<Apple, T> where T is an EatenApple— we’re using EatApple which satisfies that. Finally the Match function is used to so we can handle both outcomes. First parameter is Right the happy path, a.k.a EatenApple. The second parameter is Left the unhappy path, a.k.a. Orange.

How is this different from a discriminated union then? The key is in the Left and Right paths we talked about, and the additional logic/methods added in to create pipelining behaviour. Functions like Map Bind, which operate on the Right (happy path) allows you to chain all the happy paths together.

The really interesting part is you can clearly see all the happy path functions, ChopFruit , PutInBowl and AddYoghurt — at anytime this could fail, and if it does, it will just short circuit to Match and execute the errors => lambda function. Imagine if you had to do all the logic to handle each step possibly erroring and the if statements piling up. This is the beauty of Either<,> , it abstracts away all the nasty wiring and frees you up to understand the process.

What enables this is in the Map function. Its signature Map<TRight, T>(Func<TRight, T> mapper) , shows the parameter is a callback function and therefore will be called depending on if the Either is in a success or fail state.

A Practical Example

Cars, Fruits, Animal hierarchies are interesting n’all but lets actually get the theory into a practical example that we’re all going to come across.

In the last post we started with this:

Now, we have this:

Oh yeah! We’re using Discriminated Unions to represent all the different error states that we can get, and we’re using the Either type to describe the two paths it can go down! This is great for two reasons:

  1. Highly descriptive errors, described in types that the compiler can consume
  2. Our signature is not lying to us, we can either get an error or its done

In the last post I mentioned that we can’t get rid of some things, like handling a bunch of exceptions with our business logic is stuck in the middle of it — we can’t get away from that, but we can improve it:

Holy moly, I know, a lot more code. You can easily split this out into a HttpRequestErrors.cs, a HttpResponseErrors.cs and SendIt2_0.cs . The Send method now has a callback parameter that gives the caller a HttpResponse and they can return either a HttpResponseErrors or the result T . Kind of similar to the Map method, yes? Based on some condition we execute the happy path method — this is not by accident.

If you notice the MapLeft(resErrors => (HttpSendErrors)resErrors) is taking the signature Either<HttpResponseErrors, T> and turning it into Either<HttpSendErrors, T> allowing both HttpRequestErrors and HttpResponseErrors to be returned from the same the method. Now, this isn’t the only solution, you could just return HttpResponse and chain another call onto that, but you might end up with essentially the same thing here.

If you were wondering, yes, this is a very good example of the Open Closed Principle. We can add as many HttpResponseErrors as we like, and as many T ‘s as we’d like without changing the core process. This means once we’ve written this Send and added all our polly resilience, we never have to touch it again… pretty cool, eh?

Let’s see what it’s done for the callers.

Here’s our record defining a Github user.

Now our original exception handling code:

Exception style Program

Obviously I can put in more detail, create more exceptions to handle more cases but I could start to fall foul of accidentally not handling all of them — remember, I’m not getting help from the compiler here! It’s pretty much a free-for-all.

Lets compare with 2.0:

Woah, firstly there is more code — that’s because I handled more cases with errors, it would look way worse for the exception style program to handle all these different cases inside the try catch.

Secondly, see the many many more uses of expressions over statements.

Thirdly, if I changed anything to do with errors types, i.e. add another request error, add another response error, add another kind of send error guess what will happen… compiler goes bang! We have to handle all cases of errors, the compiler will force us to. The nice thing is it can all be in one place, at the end of the pipeline!

Thirdly.2, did you notice the throw Exception , in my estimation if there was no request given to the http client we should crash the program. In my scenario this sounds like a developer problem when we clearly manually create a request and pass it to the SentIt20 class, definitely a wiring mishap. This is an exceptional circumstance, one that will only happen once and probably never in production, so fine, the right place for an exception.

We can use the same pipelining trick that we did at the beginning by giving each step its own function:

That’s all the main logic there. I’ve kept the handle GitHub user function there because it’s part of the happy path logic.

This response handle can go in it’s own file (probably under /Application folder if you follow the hexagonal architecture and DDD style).

Finally all the error handling can go hide away in another file:

The errors are really well organised, and it’s super easy to add different behaviours for different scenarios. Doing this in multiple catch clauses could be extremely difficult (the major drawbacks of statement based code (as opposed to expressions)). Look how seamless it is to handle individual errors.

The beauty of this code is

  1. It’s readable because we have both paths in their own light (as it were) with no noise from the other (the main benefit we discussed at the beginning).
  2. There’s no if statements and try…catch statements. No real statements at all to be honest.
  3. It’s all compile time checked.
  4. Our domain is expressed clearly in the happy path and the unhappy path.
  5. The data pipeline is very clear.
  6. It’s extensible without affecting the core process (open closed principle). Want to add more errors, go ahead, want to chain the GitHub user into another process, just tag it on the end, want to add logging insert anywhere in the pipeline.
  7. The errors are expressive and quite frankly easier to sort out.

Extensibility

Console applications don’t error and return a status code of 0 (successful), neither do they write error messages to the stdout. Lets fix that change:

One simple change of returning 0 from the handleGithubUser function rather then it being a void .

Here is the new error handling:

There’s a new WriteError function which takes an area , a message and the return code , prints the error information to the stderr and returns the code given to it. It turns the previous method from a statement method (doesn’t return a value) to an expression method (returns a value).

If you’re wondering why the request errors codes start at 31 , it’s because I was giving room for response error expansion…

Finally, all methods turn from statement methods to expression methods returning an int and everything is still wired up exactly the same way as before.

Adding logging:

Time to Execute

Running it produced this:

I got my powershell to highlight the stderr message. You can see from the error, it came from the Response part, and it was unknown with a status code of 403 . We know that’s a forbidden but, also the deserialised response tells us that we’re missing a user-agent header. Easy fix!

Now, an unauthorized , I can consult the docs now on what to do about this.

Oh jeeze, easy days. Now, just before I actually got this wonderful output:

WHOOPS! I did not know you could get exceptions from adding headers, why not? Oh yeah, I remember now, there’s no freaking compile time help… you got to read the xml doc carefully… oh my days.

Yup, there it is, Exceptions:... . Wow, that could have royally screwed an application if these headers were being dynamically set from data munged together in the app.

Closing Thoughts

I will be digging into Either in more detail in upcoming posts. There’s some “quirks” that if you don’t know how to work around can be rather painful. This leads on nicely to the fact that, no, this isn’t a perfect solution (nothing is), it does have it’s drawbacks — type alignment is one of them i.e. getting the generic type constraints to marry up with what the functions are returning.

so whatever solution I propose next doesn’t actually fix many of the core problems (like goto or library authors sweeping the carpet from under you) but it will make your code way easier to write/maintain

I said this in the last post, and I think we’ve stuck to it. We haven’t fixed the core issues as you’ve seen twice in this post. First the http client is still surrounded in Exception handling in order to map it to Errors. Secondly, method signatures lie to you like Headers.Add and can throw exceptions that are never made clear to you.

Over and above all, because we abstracted away the error handling, isolated Exception handling and wrapped it in Errors has meant our core business logic and application code is easier to write in most cases and easier to maintain.

Also, here’s an F# example of the above:

The Domain
Error handling
Main Application

F# was done in 104 lines, C# was 140 lines. Obviously F# is winning in the domain expression, being able to express the whole domain in 15 lines where as C# was 33 lines. The other noticeable difference is that F# doesn’t have a whole load of type annotations everywhere due to its type system, where as C# has tonnes. Compare return Error (RequestTimedOut |> RequestError) to C#’s return Either<HttpSendErrors, T>.Left((HttpRequestErrors)new RequestTimedOut()); . Although there was no gain in characters that have to be typed, there is a gain in understanding the logic. With F# you’re left at the core of the logic with no noise, C# has a lot of noisy language syntax.

This is the interesting part, right, F# is an expression based language (everything returns a value) and has an algebraic type system allowing type resolution without the need for extensive type annotations. Each one of those concepts reduces the signal-to-noise ratio in F# apps. Introducing Either<> to C# has done the same kinda things, reduced the signal-to-noise ratio, it’s allowed us to write more expressions rather than statements and allowed us to separate the good from the bad to reduce crossing over complexity.

I will be following this up with a more in-depth look at the Either<> type, what we can do with testing, how we can engage more complex pipelines!

--

--