Exceptions – 2

At this point, it seems appropriate to put some terminology in place for my ongoing discussion of throwing and handling exceptions. As a matter of fact, I will be providing two sets of terminology for the price of one!

These sets of terminology are by Krzysztof Cwalina (leader of the effort to develop API guidelines) and Eric Lippert (senior member of the team designing the C# language). I think it’s fair to say that between the two of them there is a lot of experience with how to do things and how not to do them using C# and .NET.

Krzysztof Cwalina classifies exceptions into “usage errors” and “system errors“, the latter of which can be split into “logical errors” and “system failures“. Eric Lippert classifies them into “fatal“, “boneheaded“, “vexing” and “exogenous“.

All that may just sound like a jumble of (colourful) word-soup, so the following sections will make the terms and what they mean a bit more concrete.

Usage Errors” / “Boneheaded Exceptions
These exceptions signal that there is a problem the calling code could have avoided itself. As a result this is a bug in the caller, not a fault in the called code. Typically these are the result of broken preconditions and invariants.

public void DoSomething(string nonNullValue)
{
    if (value == null)
        throw new ArgumentNullException("value");
    ...
}

You must make sure your code passes this method a non-null value; if you do not, then the resulting exception is your own fault. You broke the contract.

System Failures” / “Fatal Exceptions
These exceptions cannot be handled under any circumstance. They signal a fundamental problem with the state of the virtual machine, such as “Out of Memory” or “Thread Aborted” or “Type Load” exceptions that can occur at almost any instruction in your program.

The only correct thing to do with these exceptions is to let them climb up the stack until they eventually terminate the program. There is nothing an application should try to do to recover from these, because there is no sensible way to recover. This is also why catching “Exception” is so heavily frowned upon.

There are ways to write general recovery handlers, but they have to follow a very specific pattern to make sense. More on that in a later post.

Logical Errors” / “Exogenous and Vexing Exceptions
These are the “real” exceptions. They indicate that the method could not make good on its promises in some fashion. You asked the method to open a file for reading, but the file doesn’t exist; too bad! You asked the method to parse a string into an integer, but there were letters in the string; oops!

The reason that Eric Lippert presents two options here is specifically for my second example. Some exceptions thrown by methods that cannot satisfy their contract indicate that the API was just badly designed; sometimes you have to expect certain failures, and code accordingly.

using (var reader = new StreamReader("DataFile.txt"))
{
    var line = reader.ReadLine();
    try
    {
        var value = int.Parse(line);
        ...
    }
    catch (FormatException ex)
    {
        // Is this Exogenous or Vexing?
        ...
    }
}

As the exception handler asks… is this exception exogenous, or vexing? I really can’t say, because it depends on the context. If the “DataFile.txt” was created by an end user, and is supposed to contain a single line with an integer value on it, then this is almost certainly a vexing exception, and use of the “int.TryParse(...)” method would have been more appropriate. Betting on a human-generated file to contain correctly formatted input is wishful thinking; you have to assume there may be problems.

If however that file were produced by another application, then this may very well be an appropriate way to deal with the situation. We can safely assume that if the other program produces an integer one time, it will likely do so every time, and if it doesn’t that is genuinely worthy of an exception and associated logic (albeit in reality probably a few levels further up the stack than my simplistic example).

Up Next…
In the next post I hope to distill down some initial advice on these three categories of exceptions. I will not treat vexing exceptions as a category from here on in, since a vexing exception really indicates an incomplete API that needs to be redesigned. Usually it is just a matter of adding alternatives that allow the caller to avoid the exception in favour of a more complex method contract, similar to the way the “TryParse” calls were added in the .NET Framework to many classes that didn’t have them before).

Back to Basics

For a while now I have been postponing writing a post about my progress regarding exceptions in software. I have informally formed an outline of an opinion, but I have been looking for a way to build a stronger foundation than “because I think that’s the right way to do it”.

Then, as I started straying further afield with my mind wandering over multi-threaded code, dependency injection, unit testing and mocking as well (and some others that I know I have forgotten), it occurred to me that I really should go back to basics with all this…

  • The most fundamental tool to reason about software correctness is still to think in terms of invariants over state-space and pre-conditions/post-conditions to method invocations.
  • Guides on “good coding practices” abound, but there are certain fundamental truths in most of them that are universal enough to almost be as good as “formal methods” to reason about “good code” beyond merely “correct code”.
  • Both the DRY principle (“don’t repeat yourself”) and a desire to produce self-documenting code further suggest that keeping as many perspectives on a single piece of code as close together as possible is the best way forward. The new .NET 4 Code Contracts already provide some unification between code, documentation and testing, but I think there is more possible that has not been exploited yet in this arena. Some tricks may be needed to keep aspects such as tests and documentation together with the code without overly burdening the generated assemblies with dead weight that does not participate in the execution of the code itself.

I strongly believe that C# as a language leaves us with too much flexibility in the general case. Every iteration of the language adds more interacting features, and opens up many useful possibilities as well as some that are dangerous or perhaps even plain wrong.

Some code patterns, although allowed by the compiler, just do not make any sense. There are usage patterns of exceptions that *will* compile, but really should be considered an error.

Tools like FxCop try to plug some of those holes by checking for such errors after-the-fact. Unfortunately, custom error conditions are not as easy to express in FxCop as I think they ought to be. But in principle this is definitely a path worth exploring to eliminate options that might be best avoided.

I think the rather nebulous state of this post reflects the fact that my mind hasn’t completely crystalised into a single vision of what combination of tools and paradigms I need to get to more ideal development practices. But I think I am starting to make some progress.