This site will look much better in a browser that supports web standards, but is accessible to any browser or Internet device.

Anomaly ~ G. Wade Johnson Anomaly Home G. Wade Home

May 30, 2005

Joel on Exceptions

As I said in Joel on Wrong-Looking Code, I find Joel on Software to be a good source of information on programming. However, I don't always agree with him. In Joel on Software - Making Wrong Code Look Wrong, Joel states that he does not like exceptions. He also refers to a previous article Joel on Software - Exceptions, where he first talked about his dislike of exceptions.

Rather than attempting to explain his rationale, I suggest that you read his essays before continuing. I think he makes some valid points and argues his case well, even though I disagree with his conclusion. I do not intend to try to prove him wrong, but to give a different view of exceptions based on my experience and understanding.

Joel's arguments against exceptions fall into five categories:

  • the goto argument
  • too many exit points
  • it's hard to see when you should handle exceptions
  • not for mission critical code
  • they're harder to get right than error returns

And, just to show that I'm not blindly defending exceptions, here's a few that he didn't cover.

  • not safe for cross-language or cross-library programming
  • possibly inconsistent behavior in threaded programs
  • lack of programmer experience

Let's go over these points one by one.

The goto Argument

Let's start with the goto argument. Joel states that exceptions are bad because they create an abrupt jump from one point of code to another (from Exceptions, above). He does have a point. However, the if, else, switch, for, and while all create jumps to another point in the code. For that matter, so does return.

The original letter about the go to did not refer to every kind of branch in code. The fact is that Djikstra did state that limited forms of branching like if-else statements or repetitions like while or for don't necessarily have the same problems as the go to, His point was that unconstrained jumps to arbitrary locations make reading the code almost impossible.

The thing that separates an exception from a go to is the limits placed on where the exception can transfer control. A thrown exception is in many ways, just a variation of return. It may return through multiple levels (unwinding the stack as it goes), but an exception does not jump arbitrarily in your code.

Obviously, we wouldn't be willing to give up if in order to remove an abrupt jump to another place in the code. The difference between the goto and the if is limits. The if is constrained in where it is allowed to jump. Likewise, any exception is only allowed to transfer control back up the call stack.

While it is obvious that throwing an exception is more powerful than an if, it should also be obvious that it is much more constrained than an arbitrary go to construct. So the comparison of exceptions to go to is at best an appeal to emotion. Unfortunately, it is an argument I've seen far too often.

It's Hard to See When You Should Handle Exceptions

In Joel on Software - Making Wrong Code Look Wrong, he states that one of the reasons he doesn't like exceptions is because you can't look at the code and see if it is exception-safe. Given his earlier statement about how important it is to learn to see clean, I find this statement particularly interesting.

Although I understand Joel's commentary about the difficulty of making certain that code is exception-safe, I maintain that a good use of exceptions can help create cleaner code, that is easier to maintain. The key to creating exception-safe code is using the guarantees provided by the exception mechanism. Unlike Java, C++ makes a hard guarantee, Any local variables are destructed properly when an exception leaves the scope they were created in. This has lead to the resource allocation is initialization idiom (RAII). I'll use Joel's example to explain:


dosomething();
cleanup();

This actually jumps out at me as incorrect code. What am I cleaning up? This sounds like either a resource cleanup issue or a dumping ground for a collection of things that happen to be done at this time. If it's the first case, code probably looks more like this:


setup();
...
dosomething();
cleanup();

In which case, the RAII approach would be to create an object that manages the resource. The equivalent of setup() is the object's constructor. The equivalent of cleanup() is the object's destructor. This changes the code to:


Resource res;
...
dosomething();

This code is safe in the face of exceptions and cleaner to boot. More importantly, in my opinion, the setup and cleanup of the resource is now defined in the one right place, the definition of this class. Now if I ever use the resource, I'm guaranteed that the cleanup code will be called at the appropriate time. There's no need to remember to call cleanup() at the right times.

Unfortunately, this idiom will not work for Java, since that language specifically does not promise timely destruction of objects. In that case, as Joel points out, you are required to use the try ... finally mechanism to ensure cleanup. For this reason, Java does not always allow exceptions to clean up the code.

Not For Mission Critical Code

Joel goes on to suggest that exceptions would not be useful on larger or more critical systems. In fact, a large portion of my career has been focused on long running programs (including systems with 24x7 uptime requirements). What I've found is that the code without exceptions tends to fail in strange ways because error returns are tested inconsistently, and error codes are handled differently in different places in the code. Keeping all of these different places consistent requires a large investment in time as the code changes.

Now, one could make the argument that if all of the error returns were tested and if all of the errors were handled consistently and properly, the error-return based programs would be perfectly robust. Then again, you could also make the argument that if the exceptions were all handled correctly, the code would be perfectly robust, too.

The main difference here is that if the programmer does nothing, error returns are lost by default and exceptions terminate the code by default. For this reason, exceptions tend to be more likely to be handled because they would otherwise terminate the code. If an error return check is accidentally left out, no one may notice and testing may not point out the problem. If an exception is not caught, the program is likely to terminate during testing and, therefore, the problem will be found.

I do realize that either error returns or exceptions can be ignored or handled badly by any particular programmer. But in my experience, exceptions have been harder to ignore. While I probably would not say that exception code is easier to get right than error return handling, I would say that error return code is easier to get wrong.

Harder to Get Right Than Error Returns

Handling error returns explicitly in the code tends to increase the amount of error handling code spread throughout the program to the point that it hard to see the actual code logic for the error handling logic. I agree with Joel that your best programming tool is the pattern recognition engine between your ears. I find it much easier to understand and fix code logic if I can see it, without a large amount of extraneous code in the way.

In many ways, error return checking and handling code (if done completely) can obscure the main logic of the code to the point that maintenance becomes a nightmare. To extend Joel's cleanup example a bit, let's see what error return checking code does when you need to perform cleanup.


setup();
doSomething();
doSomethingElse();
doOneMoreThing();
cleanup();

Let's say that each of the functions above returns an error condition that we need to check. Let's assume further that we need to return the error from this routine. Finally, we will need to perform the cleanup in any case. So the code might end up looking like this.


ErrorType err = SUCCESS;
if(SUCCESS != (err = setup()))
{
// no need to clean up.
return err;
}
if(SUCCESS != (err = doSomething()))
{
cleanup();
return err;
}
if(SUCCESS != (err = doSomethingElse()))
{
cleanup();
return err;
}
if(SUCCESS != (err = doOneMoreThing()))
{
cleanup();
return err;
}
cleanup();

or like this


ErrorType err = SUCCESS;
if(SUCCESS != (err = setup()))
{
// no need to clean up.
return err;
}
if(SUCCESS != (err = doSomething()) ||
SUCCESS != (err = doSomethingElse()) ||
SUCCESS != (err = doOneMoreThing()))
)
{
cleanup();
return err;
}
cleanup();

or any of several variations on this theme. In any case, the call to cleanup() ends up duplicated.

In my experience, anything that is duplicated will probably be left out at some point during maintenance. More importantly, if the cleanup is more than one statement instead of a single function call, the different places where it is written are likely to get out of sync. Careful code reviews can reduce this problem, but it still hangs over the process.

More importantly, to my mind, is the fact that the actual logic of the program is now obscured by the mechanics of the error handling. This is the problem that exceptions were created to reduce.

If we once again apply the RAII idiom this code becomes.


Resource res;

doSomething();
doSomethingElse();
doOneMoreThing();

In this code, it is much easier to see the main line of the logic. What you can't see is exactly what exceptions might be thrown. But, the original code did nothing specific with the error returns, it just passed on the error condition to the calling code. This code does the same, it just does it in terms of exceptions.

Cross-Language/Library Programming

Unfortunately, the C++ standard cannot promise compatibility of exception across language boundaries. There are almost no guarantees if your C++ code calls a C routine that uses a C++ callback that throws an exception. Obviously, this is not a situation you'll run into every day, but it is a problem. This issue may also apply to JINI code that uses C++.

A bigger problem is the fact that exceptions are not necessarily binary-compatible between modules, even in C++. It is possible for a library compiled with different options to produce exceptions that do not quite match the exceptions from the rest of the code. (See C++ Coding Standards for the details.)

In the worst case, this just means dealing with error codes at the boundaries of these two cases. So it is no worse than dealing with error returns normally.

Behavior in Threaded Programs

Some C++ implementations may have difficulties with exceptions thrown in threaded code. In the past, I've seen exceptions improperly propagated into the wrong thread. Most modern C++ compilers and libraries should have solved these problems. As far as I know this has never been a problem in Java.

Lack of Programmer Experience

This is probably the biggest problem with exceptions. A large number of programmers are not very experienced in using exceptions correctly. I've seen exceptions used in cases where an if or switch would have been much more appropriate. I've seen cases where an exception was caught as a generic exception and then a series of type tests were performed in the catch block. I've seen people use empty catch blocks to ignore exceptions.

The solution to this problem is education and experience. Unfortunately, this one takes time. Fortunately, many of the bad practices above can be found relatively easily in code reviews.

Conclusion

I would like to conclude this (overly long) essay with a summary. My goal in this essay was not to prove Joel Spolsky wrong in his dislike of exceptions. My purpose is to give an alternate view. My experience has shown (to me at least) that there is quite a bit of truth in the arguments against exceptions. On the other hand, I have seen several cases of exception usage increasing the robustness and correctness of large and small programs.

To link this back to Joel's essay, the main issue here is the need to learn to see well-done exception handling. This is something that our industry needs a lot of practice with. However, exceptions are a powerful tool and ignoring them may not be a viable technique in the long run.

Posted by GWade at May 30, 2005 04:00 PM. Email comments