Exceptions – 5

Before diving into the final category of exceptions, I want to make a little detour into fundamentals. I promise this will be relevant shortly.

Preconditions / Post-conditions / Invariants
Formal Methods and Design by Contract are often a dim memory after a few years in a full-time software development job. It would be easy to conclude that therefore they do not get applied in real business. But in actual fact, contracts are everywhere.

Whenever you write a method, it’s name, result type, parameter names and parameter types are part of an ad-hoc specification covering preconditions and post-conditions.

int Square(int n);
void ValidateOrder(Order order);
Order MergeOrders(params Order[] validOrders);

I should be able to reasonably assume without looking at the code that:

  • Square” will return me the square of its argument
  • ValidateOrder” will probably throw some exception when the contents of “order” do not meet validation standards
  • MergeOrders” will create a new single order object out of a collection of other orders, provided they can be combined (and if not, likely throws an exception). Also, the name of the argument strongly implies that validation may need to be done prior to calling it.

It is of course possible that the names and types are misleading and these methods do something completely different, but in that case I’d argue that they are not meeting their implicit contracts.

Compare this with the following signatures were they to have exactly the same implementations:

int Calculate(int n);
void Process(Order order);
object HandleTogether(IEnumeration toCombine);

By simply changing some names and types I have destroyed a lot of the implicit documentation this method provided:

  • There is no indication what relationship there is between inputs and outputs for “Calculate“. Even worse, I can no longer reasonably assume this method succeeds for all integers “n” without looking at the documentation or implementation.
  • The name “Process” although technically accurate (but then, isn’t everything processing in some sense?) gives the misleading impression that it might in some sense execute the order. Exceptions could still be expected if processing fails, but it might prompt a defensive implementation predicated on the false assumption that there may have been side-effects.
  • And “HandleTogether” pretty much completely obscures both the nature of the operation and the preconditions that must be satisfied by its arguments. Let’s hope the documentation comments are actually helpful!

As these examples already alluded to, exceptions logically form a part of the specification of a method.

/// <summary> ... </summary>
/// <param name="validOrders"> ... </param>
/// <exception cref="ArgumentException">
/// validOrders == null || validOrders.Length == 0
/// </exception>
/// <exception cref="ValidationException">
/// Any of "validOrders" fails validation.
/// </exception>
/// <exception cref="MergeException">
/// Not all "validOrders" have the same customer details.
/// </exception>
Order MergeOrders(params Order[] validOrders);

There could potentially be a lot more involved, but now the exception documentation confirms and enhances the specification implicit in the method signature itself.

Note that there is one further improvement that could be made above; currently the first exception is an “ArgumentException“, which therefore corresponds to a precondition (see Usage Exceptions post). The “ValidationException” is presumably the exception thrown by the “ValidateOrder” method that we’d be using internally to make sure all the orders are valid before attempting the merge. And the “MergeException” is a new exception specific to this method that indicates incompatible orders.

In reality, all these should probably be preconditions to the method, and therefore be implemented as “ArgumentException” instances. It is in most cases much better to fail early before any calculations have been done.

Vexing Exceptions
In practice, “Vexing Exceptions” pretty much need to be dealt with in the same way as any other “Logical Errors”, but they indicate a badly designed API (see original overview post). In the remainder of this post I will not treat them separately, but I want to dedicate a few moments here to recognising and avoiding them when writing new code.

In the previous section I had a few example methods to illustrate extending method signatures with exception specifications.

/// <exception cref="ValidationException" />
void ValidateOrder(Order order);

/// <exception cref="ArgumentException" />
/// <exception cref="ValidationException" />
/// <exception cref="MergeException" />
Order MergeOrders(params Order[] validOrders);

And I commented that the “ValidationException” in “MergeOrder” corresponded to a possible result of using the “ValidateOrder” method, and that all conditions on “MergeOrders” would be better served being “ArgumentException” across the board.

To do so for the validation exception would mean that “MergeOrders” needs to implement a catch handler during its precondition checks and wrap a “ValidationException” into a descriptive “ArgumentException“. This is precisely a “Vexing Exception”, because we would be much better served by a second API variant of “ValidateOrder” that returns errors, or even just a boolean:

IEnumeration<ValidationError> ValidateOrder(Order order);
bool TryValidateOrder(Order order);

Then we can do the validation in our merging routine without having to catch exceptions and wrap them.

Whether or not transforming a “MergeException” into a “Usage Error” makes sense depends on a number of factors, including whether an up-front check would have to re-implement substantial portions of the logic from the body of the method. Sometimes it may be better to leave the exception unchanged.

Note however that either way we really need a further method:

  • If we make it a precondition, then the caller needs to be able to avoid passing in incompatible orders
  • If we leave it unchanged, we have another potentially vexing exception in case compatible orders cannot be guaranteed

The caller of the merge method needs to either structure the code so that it is implicitly guaranteed that orders passed into the method will be compatible, or there needs to be an “bool AreOrdersCompatible(...);” method so that a failing call can be potentially avoided where it might otherwise routinely occur.

Logical Errors” / “Exogenous Exceptions
And now that we have eliminated everything else, what exactly are we left with? It turns out that I lied in the last section… I am not quite done with “Vexing Exceptions” yet.

Since “Vexing Exceptions” are thrown under expected circumstances, where providing an API alternative that directly returns a result indicating those circumstances is the preferable approach, I think “Exogenous Exceptions” can best be summarised as follows:

“Exogenous Exceptions” correspond to unexpected circumstances that cannot be avoided but can potentially be resolved by the caller.

So, in a perfect world:

  • “Usage Errors” are only ever thrown and never caught, because they indicate a caller that does not respect preconditions
  • “System Failures” are only ever thrown by the environment and never by code and they are never trapped, because they indicate the environment has become unreliable and the application should be allowed to terminate
  • “Vexing Exceptions” never occur because all our APIs provide method alternatives to avoid them
  • “Exogenous Exceptions” are only ever thrown if a method cannot satisfy its post-conditions due to unexpected, unavoidable circumstances outside its control, and each type of exception corresponds uniquely to one type of remedial action

Let’s start with an illustrative example of a “Logical Error” from the .NET Framework itself.

try
{
    using (FileStream fs = new FileStream("...", FileMode.Open))
    {
        ... load resource ...
    }
}
catch (FileNotFoundException)
{
    ... load from elsewhere ...
}

As I was trying to come up with a good example of an “Exogenous Exception”, it became more and more clear to me that in-principle there are none. Every time an exception is thrown by a method, the question remains: is this an expected or unexpected exception? And the only code that can answer this question is the caller.

In the fragment above, it is tempting to say something along the lines of “you cannot avoid this exception; it is thrown when a file does not exist when you try to open it“, but somewhere in the implementation of the “FileStream” constructor, there is a line that determines whether the low-level Windows API succeeded or failed, and turns that into an exception. If I write code using the “FileStream” API, where I can routinely expect files I am trying to open will no longer exist, then this is suddenly a “Vexing Exception”.

The only reason I have no choice but to use an exception handler is that using “File.Exists(...)” does not help, because the file may go missing between calling this and the “FileStream” constructor. And there is no constructor alternative “FileStream.TryCreate(...)” that can allow me to normally handle this condition. Vexing indeed.

Note however that this does not mean that all is lost, and the naysayers about exceptions were right after all. Far from it. I think “FileStreamshould throw an exception if the file does not exist. But it should also have an alternative that doesn’t.

And this goes for all methods, because ultimately the only arbiter of what is expected to go wrong (“Vexing”) and what is not (“Exogenous”) is the calling code; it’s the use that determines the nature of the exception.

(Sidenote: this possibly explains the ongoing religious war over whether exceptions or error codes are the best way to handle errors. Those against exceptions tend to look at “Vexing Exceptions” as their rationale, whereas those in favour can only see “Exogenous Exceptions”. It turns out we really need both.)

To Be Continued…
I was going to finish up here with a description of how to implement methods, how to consume methods, and what can be done to formalise and automate some of the required discipline in all this…

…but this post is getting a bit long already, and I think having just the guidelines in a single post will provide a better reference.

(Really just a stalling technique so I can let my most recent lightbulb-moments filter into this before I come to a final decision.)