Sunday, November 3, 2013

Let's design better exceptions

I've always been sort of mixed towards exceptions in object oriented languages. When done right, they are pretty neat, but in other cases, they are a source of endless frustration.  I've been thinking about why this is, and how we can change them for the better.

The sort of exception-handling code I have nightmares about looks something along the lines of

This sort of code is very unpleasant to work with. As a caller, I am forced to deal with more exceptions than I want to.  The second and third exceptions should have arguably been run-time from the get-go. Now I either have to handle them poorly (re-throw, ignore), or infest my code with even more checked exceptions that just push dealing with them higher up the call hierarchy. That may be okay within the same module, but once your aggregate starts publicly hemorrhaging unchecked exceptions from its components, something has gone wrong. It rhymes especially bad with modern code that is going to be glued together with dependency injection -- it just doesn't work to leak implementation details like that anymore.

There is a deeper issue afoot here, what really should be gnawing at your design senses is that this pattern simply isn't kosher object-oriented design. I shouldn't be inspecting the run-time type info of an object, and then determining behavior based on this! This is all sorts of wrong!  We are encoding what reasonably speaking should be data into the object type, so that the only way to extract the data is a bunch of glorified instanceof clauses.

If I were to speculate why exceptions look the way they do, I'd say it's because they were designed by the same people who wrote the standard library, rather than those who spend their days writing business logic. Which is a phenomenon you see every once in a while.  C++ does the same thing with operator overloading -- a rather large language feature that really only shines in designing custom numerical types and collections. The same way, Java exception handling makes perfect sense if you're writing an I/O or threading library, but they are wholly unsuitable for application development.

A few observations to act as a base for a new exception paradigm

  1. The caller shouldn't be forced to handle errors against their will -- programmers are lazy creatures, and this will lead to bad code.
  2. The caller shouldn't have to use instanceof to glean information about the error.
    1. The caller may want to know the type of error.
    2. The caller may want to know the source of the error.
    3. The caller may want to know if the error is recoverable.
  3. Our exception-system must be as extendable as regular exceptions. Lists of magic integers or strings are unacceptable as type-information.
  4. If the callee throws an exception, it does not know how the caller wants to handle it (i.e. you can't have a fixProblem() method in the exception).
Applying these observations, I've come up with an ansatz that looks like this:

It's rather difficult to see how this behaves looking at the code itself, so let's apply it to the original scenario.

The difference may appear subtle from the original examples, but look what really happened here: First of all, the callee does no longer need to let us know every way it can fail. By declaring it throws a BetterExceptionIf, it tells us it can fail and we need to know about it, presumably because it thinks we can recover. I'm a bit undecided whether I consider an unchecked exception may be more appropriate here.

When it does fail (either directly, or some in some unhandled sub-component exception), we are not forced to to concern ourselves with exactly which manner it failed in order to handle it responsibly. The caller can glean as much (or as little) information as he requires in order to deal with the problem.

No comments:

Post a Comment