The Achilles’ Heel of C#: Why its Exception Handling Falls Short

mckoder
6 min readSep 17, 2023

--

The Unpleasant Surprise

My first experience with C# exceptions, after having been a Java coder on multiple projects, wasn’t good. My carefully written, rigorously tested C# code suddenly started crashing. What happened? My code relied on a library written by someone else, and the owner of that library had changed a method to throw a new exception. Since my code wasn’t prepared to handle that exception, my code crashed. Had this been Java, the compiler would have alerted me to the fact that the library method is now throwing a new exception and made me update my code.

Compiler Checking is Good

In Java, exceptions are checked by the compiler (“checked exceptions”). The compiler forces methods to declare what exceptions it can throw. It forces callers of the method to decide what to do with each possible exception. If you know how to recover from the exception, you catch the exception. If you don’t, you’re obligated to declare it using the throws keyword. Consequently, the ball gets passed to the caller of your method to deal with the exception.

If the benefits of declaring exceptions and having the compiler check them are not immediately obvious, read on.

Recovering from Exceptions

“Recovering” from an exception entails gracefully handling the error to ensure the program continues to operate smoothly. For example, if a file could not be opened because it doesn’t exist, you may want to create it. In this case you catch the FileNotFoundException exception. That’s a well-known example, so it is obvious what exception to catch. But sometimes you want to recover from application-specific errors. Maybe you want to try a customer’s backup credit card if the primary credit card is declined. Now it is not obvious what exception to catch. There may be more than one exception to catch, for example, CreditCardExpiredException, SuspiciousActivityException, CardTypeNotSupportedException and so on.

C# Exception Recovery: You’re Blindfolded

In C#, methods do not declare what exceptions they can throw. So how do you know what exceptions to catch, in order to recover from errors? Check the documentation? Talk to the developer who wrote the code you’re calling? Test your code extensively to see what exceptions get thrown? Unfortunately, each of these approaches is fraught with pitfalls. Documentation may be incomplete or outdated. The developer who wrote the code may not remember which exceptions get thrown. And you never know whether you have tested exhaustively enough to find all exceptions.

Consider that exceptions can originate not just from the method you’re directly calling but from any methods that it, in turn, invokes — ad infinitum. The task of manually tracking all potential exceptions — as required in C# — becomes a Herculean feat. To make matters worse, any of these underlying methods could undergo modifications without your knowledge, rendering your hard-earned list of exceptions obsolete.

Introducing a New Exception is Hazardous in C#

When you modify a method to introduce a new exception, you also need to update any calling methods that handle exceptions originally thrown by your method. But how do you know which those methods are? In Java this would be easy, because the compiler will give you helpful error messages. In C# the compiler offers no help, and so there is no easy way to identify the code that needs to be updated. (Keep in mind that the code that needs to be updated may be several levels up the call stack!) If you miss some locations that need to be updated then at those locations the program no longer recovers from the error like it used to, and thus the reliability of the program goes down.

No Clean Way to Remove an Exception in C#

Conversely, imagine revising your method so that it no longer throws a particular exception. Now you’re left with vestigial exception handlers that never get used. Locating and removing them is no small task, and the absence of compiler guidance in C# means that such dead code often becomes a permanent fixture in your codebase. In Java the compiler will tell you what code to remove.

The Takeaway

In summary, C#’s approach to exceptions poses significant challenges, especially when compared to Java’s robust and compiler-checked mechanism:

  1. Uncertainty in Error Recovery: Unlike Java, C# doesn’t offer a comprehensive list of exceptions that a method can throw, leaving developers in the dark about which errors to anticipate and how to recover from them.
  2. Fragile Recovery Logic: When existing methods in C# begin throwing new types of exceptions, it becomes a guessing game to determine which error recovery code in your application needs to be updated. This fragility can compromise the stability of your application.
  3. Orphaned Exception Handlers: In C#, identifying and removing “dead code” — exception handling blocks that are no longer used — becomes a convoluted task, whereas Java’s compiler assists in flagging such issues.

Appendix: A real-life example

In my C# application, I implemented a RemoteStorage class that talks to a web service for storing artifacts. The web service then uses Azure Storage for storing the items. My RemoteStorage class uses AJAX to talk to the web service, so it throws AJAXException in case of errors. Code that needs to recover from the error catches AJAXException.

A few months later I revised the implementation of RemoteStorage to use Azure Storage directly instead of going through a web service. This meant that the methods in this class no longer throw AJAXException, they throw Azure.RequestFailedException. Now I need to update the calling methods that expect to recover from errors. But how to find the code? Keep in mind that the code that needs to be updated may be several levels up the call stack! I don’t want to blindly replace all instances where AJAXException is being caught, because some of them may be unrelated to RemoteStorage calls!

Because the C# compiler does not offer any help I am on my own. Many instances of catch AJAXException is now dead code, but it is hard to identify them. Many instances where code was previously recovering from RemoteStorage errors now have an uncaught exception, but it is hard to identify and fix the code. Why doesn’t the compiler help? The Java compiler would!

FAQ: Why not just catch the Exception base class?

You may think you can work around C#’s limitations by always catching the Exception base class instead of more specific exceptions. But wait. If you catch the Exception base class, you will end up swallowing some exceptions. “Swallowing exceptions” occurs when code lower in the call stack captures exceptions but fails to handle them adequately, thereby preventing higher-level code further up the call stack from receiving and properly addressing those exceptions.

For example, if you indiscriminately catch Exception (instead of CreditCardExpiredException, SuspiciousActivityException, CardTypeNotSupportedException and so on) in order to recover from a credit card transaction failure, you may inadvertently catch SQLException as well, but that is a different type of error (i.e., the credit card transaction was successful, but the database update failed), for which charging the customer’s backup credit card is not the appropriate recovery! Presumably there is code higher up the call stack that handles SQLException, so you don’t want to swallow it.

Not to mention, if you catch Exception you catch NullReferenceException, IndexOutOfRangeException and so on, which would hide bugs in your code.

What not to do: Defeating Java’s checked exceptions

Some Java developers who are unfamiliar with the benefits of checked exceptions wrap exceptions in RuntimeException and throw it. Now you avoid the inconvenience of having to declare exceptions. Is this a good idea? No, because you may save some typing, but maintenance becomes harder for reasons described in this article. Which is more important? Saving typing during initial development? Or ease of maintenance, reliable recovery from exceptions, and avoiding dead code?

RuntimeException and its subclasses are unchecked exceptions that don’t need to be declared. It is intended only for use in situations where you’re sure no error recovery is possible or should be attempted, and you want the exception to crash the program.

--

--