Stop using Exceptions

Callum Linington
8 min readFeb 22, 2024

--

Exceptions are so bad… they have their uses but people seem to abuse them way past their actual usages. Also, the implementation of exceptions in .NET is bad and have generated some horrible pervasive patterns…

When developing an application we shouldn’t be arbitrarily throwing exceptions and we should definitely not be doing MyApplicationException : Exception as if we’re doing ourselves a favour by creating “domain” exceptions.

We all should be aware of “Exceptions are for exceptional circumstances”, they can also be for crashing an app on purpose. For instance it could be that a dependency wasn’t given to a class — I would want that to crash immediately, there’s no way on God’s green earth can we continue without a dependency being there, fine, throw new MissingDependencyException(nameof(IMoneyProcessor)) . These are the acceptable use cases.

I don’t usually try and word things like “don’t do this” or “do this”, I much prefer to show my side of the story and let the reader make the decision — of course you still can make the decision — but this is such a endemic thing that creates so many problems, it’s worth addressing it head on.

Exception Implementation is Bad

Let’s address the first point I made, the implementation is bad… this is fairly straight forward, look at this code:

It doesn’t matter what IDE you open up, what compiler you build with, nothing will ever tell you that DoSomething could throw an exception or you’re not handling an exception that could be thrown.

I mean, how are we able to compile this code, it throws an exception, it’s never caught… why even bother wasting CPU cycles.

To compare this with other languages, Go routines returns two values from a function, one is the error and the other is the actual result. Java has the throws keyword that allows you define the exceptions in your method signature which means the caller can benefit. Rust has the Result type which allows the caller to integrate the response (much like the way Go does, but works slightly differently). I’m not saying these are the best solutions (although the rust one is very close), I’m saying these are way better than C#.

The issue is that C# allows you to arbitrarily throw exceptions without any compile time information to help callers out (bar the xml docs, which doesn’t actually go any distance to solving the problem). What these means for us is that we have no idea if the code we’re calling will throw exceptions and the signature is probably lying to us and we have to do horrible things like long catch statements to catch everything under the sun that we might have to handle… take this HttpClient.SendAsync method for example:

That exception documentation comes from the xml doc that we talked about earlier. Problem 1: nothing is enforcing us to handle those exceptions, they could just occur and crash our program… why……. Problem 2: the signature is a lie, it won’t just return a HttpResponseMessage , it could throw an exception, Problem 3: The code to handle all that is gunna look like garbage… and what do you do with all of that information?

Like I say in the HttpRequestException comment, we should probably be nesting this in a while loop to do retries (or use the polly library) which is turning into a crazy amount of code. Our actual logic is 1 line, now it’s 26 to include all the exception handling. That means our core process that we care about is now nestled in a tonne of (quite frankly) useless code.

I would also just like to point out the snarky comments in each catch block. Unless you read the xml doc you have no idea why each exception gets thrown. Like, explain to me how a TaskCanceledException amounts to a request time out… why not an error class like RequestTimedOut — let me know which one you found easier to understand at face value.

Wait, it gets worse, lets now put this into a proper process chain

SendIt Class
Program

Now we’ve separated out our http calling logic to it’s own code, handling those exceptions.

Imagine this new scenario, rather than use MyApplicationException we brought in a new exception type, SendItException just to be a little bit more specific. The IDE will not let us know that our program is now catching the wrong exception… C# is not built like that — you’re on your own buddy. Worse yet, take this one step further, what if Microsoft changed the way HttpClient.SendAsync throws exceptions… now we’re really up the creek because our lovely retry logic that is being driven off of a HttpRequestException or TaskCanceledException will now just fail. Good stuff…

This doesn’t happen with normal code, int Calculate(int a, int b) if that signature ever changed, the compiler would be in there like a shot — firing off errors left, right and centre. It would leave no usage reference unchecked. This is the power of the type system right?

We should probably find a solution that uses that incredible power (foreshadowing…).

Pervasive Patterns

Firstly, I’ll just get this out of the way, we all know code that is doing try { } catch (Exception ex) { } right! A lot of the time, you’ll probably just do this so you can give yourself the chance to log this exception somewhere for later debugging purposes… good luck with that, lets hope the exception isn’t an unrecoverable one — check out all the exceptions that can occur https://learn.microsoft.com/en-us/dotnet/api/system.exception?view=net-8.0 (use the view more on the derived type, if you missed it, here is a whole lot more https://learn.microsoft.com/en-us/dotnet/api/system.systemexception?view=net-8.0).

The next pervasive pattern is this, the TryXXX pattern:

The main problem with this, it only works in really simple scenarios due to no specific knowledge on why it failed. If you use this in your business logic you’ll start to really annoy your users because the only real message is didn't work, probably due to your input being wrong which doesn’t help the user fix their input.

The final and probably worse pervasive pattern is a form of exceptions being used as control flow. Which, arguably, is what we’ve describe above in the HttpClient examples. But, this one is worse. The idea revolves around the fact that throw is a goto statement in disguise, and there are instances where code deep in a process flow throws an exception and it is caught all the way at the top of the process.

Hopefully this diagram has been able to visual that pattern. You can see that the flow is MyProcess -> Step 1 -> Step 1.1 -> Step 1.1.1 -> Some Process each one is a call into an inner object. You can imagine that this exacerbates all of the problems we mentioned above — yet is still used as like a get out of jail free card. It makes doing things like transaction rollbacks and the likes less realiable because you can forgo any “undoing” that Step 1.1 could be doing (for example).

Closing Thoughts

It is pretty much doom and gloom from a C# language perspective and a .NET framework perspective because exactly like null, Microsoft rely heavily on this “language feature”, 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.

It’s very easy to create exceptions, throw them and maybe handle them later, and you may think well I’ve got tests — but this still suffers from no compile time help. It’s interesting to see a language grow, and C# is pretty old now, we see a lot of syntactic sugar in new releases (primary constructors come to mind, and the experimental green threads) and being that the language has only really been in addition only mode for the last nearly 20yrs it’s no surprise that there are still some horrible things about it.

One thing I always like to think about, is that when we’re solving business context problems we should be creating abstractions that help with understanding that business context (DDD thinking) and exceptions are a low level tool (or a get out of jail not so free card if you really think about it). We should just bite the bullet and accept we’re going to need to write a little bit more code than just what the frameworks provide.

On a slight tangent, I reckon Microsoft should do what they did with “.NET Framework” going to “.NET Core” where they completely gutted the framework, removed all the windows dependencies and made some vastly substantial breaking changes — this should be done to C#. Lets have a C# Next, where we take a look at all the core features and strip out all the backward compat with C and C++ (in the sense of being able to entice those devs over to .NET).

Features that seriously need looking at including exceptions are: nulls, reducing syntax for Classes (auto get, set properties, backing fields, expression bodied stuffs, primary constructors, records), Not using Objects for everything (top level statements only apply to program at the moment, but they should be everywhere) — are there any that you can think off that are in dire need of simplification/revamping?

I will be providing a solution in the next one, just wanted to prep you the problem statement.

--

--