About Chess and Nuclear Reactors: The Case for Exception Handling

The world of software development has more than its fair share of topics where people tend to have long religious discussions about the “correct” way to do something. I think this is partly because the field for some reason attracts the kind of person who enjoys a nice bout of verbal fisticuffs, and partly because we spend a lot of time dealing with very abstract topics where the pros and cons of a given choice have more to do with differing philosophies than with objective facts.

One classic topic for this kind of discussion, which came up recently at work, is the use of exceptions for error handling. Every modern programming language offers an exception mechanism for this purpose, and presumably it is there to be used. However, ever since they were first introduced, there has been a large and vocal subset of the community arguing that exceptions do more harm than good and you’ll be writing better code if you just use good old return values to report whether a method succeeded.

One representative example comes from Joel Spolsky, one of my favorite authors. Another oft-quoted article making the same arguments is found in the “Frequently Questioned Answers” by Yossi Kreinin. They both make the same basic points: exceptions do not reduce complexity but merely hide it, and when complexity is hidden people tend to forget about it.

These arguments have merit, but I still feel that (when properly used) exception handling delivers enough value to be worth the cost. So I am going to be arguing for the status quo here, for a change. Executive summary: the dangers of exceptions are real, but code readability trumps almost everything.

Let’s say that we have a method of, say, 15 lines of code. This method is straightforward and easy to read and to modify. I can understand at a glance what it does; it forms a clear picture in my mind which I can easily reason about. There is just one problem with it: We haven’t added any error handling yet. For which we have basically two options: use return values, or use exceptions.

If we use return values, our nice clean 15-line method will suddenly blow up to easily five times as large, with lots of conditional statements and lots of ugly, repetitive boilerplate code. Such code is annoying to write and to read, easy to make mistakes in, and very tempting to get sloppy with. Suddenly, I can no longer hold a mental picture in my mind of what the method actually does. I’d have to go through the code line-by-line and trace all the possible code paths. Even then, I am quite likely to miss some subtlety. And since the method is now too large to fit on a screen, I should refactor it into multiple smaller methods, which requires adding even more errorhandling boilerplate.

On the other hand, if I use exceptions instead, most of my methods will become only a little larger, and all of the errorhandling code will be nicely contained in one place where I can reason about it separately. The rest of the code stays nice and clean and easy to understand at a single glance.

“Aha!” say the exception skeptics (exceptics?). “But that reduced complexity is only illusionary! If every line of code in your program might thrown an exception, you still have an enormous amount of possible code paths to worry about — they’re just not explicitly visible in your source code anymore, which is even worse!

Well yeah, that’s true. I still have to worry about those exceptions even if I can’t see them. But the nice thing is, I can think about them, because we are now back in the scenario where I have just 15 lines of code which I can easily turn into a mental picture which I can reason about. I can look at that code and ask myself: “What happens if an exception is thrown on this line?” “Will the resource I allocate on line 4 be properly cleaned up in all possible scenarios?” “If this function fails because of an invalid input file, how will we recover from that?” And the big difference is that I can reason about these questions at a much higher level of abstraction.

It’s like the difference between an amateur chess player and an expert. When a novice player thinks about his next move, he works his way through the possible options as a computer would: “I could do this, but then my opponent will do either that or that. Or I could do this, but then she will do …” This is not a good way to play chess, since there are a lot of different combinations and the human brain is really bad at systematically working its way through such a huge decision tree.

An expert chess player, on the other hand, thinks at the level of strategy and tactics, rather than individual moves. “I am on the offensive, but my opponent has stronger control over the center field. It looks like I could attack over the left flank and win a few pawns, but I should probably do something about that exposed bishop first..” Only after analyzing the board at this high level, will the expert identify a small number of specific opportunities and threats to be investigated at the level of individual moves and counter-moves.

From a given starting position, the number of moves and counter-moves available to the expert is the same as to the novice. But by thinking about the game at a high level of abstraction first, the expert can dismiss the vast majority of possibilities at a glance, without conscious thought, and focus on the half-dozen interesting cases which need to be investigated in detail. It could happen that the expert overlooks some subtle trick which the plodding novice would have discovered, but the smart money will bet against it.

(As an aside, research has shown that experts are much better than novices at memorizing the position of the pieces of a chess board, but only when dealing with a position from a real game. When the pieces are placed on the board randomly, the experts are not much better at memorizing them than people with no knowledge of chess at all.)

I am hardly a chess master, but I’m fairly good at reading code. When I try to read a large block of ugly code where every “real” function call is followed by a dozen lines of boring error handling code, I feel like the novice chess player in the story above, missing the forest for the trees. When reading a nice clean piece of code where the error handling is provided through exceptions, I feel like the expert player, spending most of my time thinking at a higher level of abstraction. The complexity of the situation is the same either way, but now I have a mental picture of the situation which allows me to visualize it from different angles, dismiss all of the boring straightforward cases at a glance, and quickly zoom in on the ones which require my detailed attention.

At this point, assuming you’re still reading, you are probably either nodding along with me or you’re getting red in the face and starting to sputter in protest. If you’re nodding along, that likely means you belong to the camp which views programming as an essentially artistic activity, where intuition and the undefinable concept of ‘elegance’ play a big role. You believe that there is an unprovable, but very real, connection between the subjective beauty of a piece of code and the chance that it is correct.

On the other hand, if you’re sputtering and getting angry, you probably belong to the camp which feels that software development is an engineering discipline in which feelings and emotions have no place. Your standpoint is that if a piece of code has 200 different possible execution paths, then somebody should bloody well go over all of those 200 paths and verify that each of them is correct — anything else is sloppy and amateurish and would never be allowed in your team.

And to be fair, I have a lot of sympathy for that standpoint, as I like to think of myself as a left-brain type of person as well. In fact, if I were in charge of developing the control software for a nuclear reactor, I would change sides: I would disallow exceptions, make sure that all conditionals are written out explicitly, and then use a variety of formal proof techniques to guarantee the correctness of every single path through the code. My team’s productivity may be less than one line of code per day, but there will be no mushroom clouds on my watch!

At this point, the people in the second group puff up their chest and say, well maybe you think that your project is not important enough to be worth using proper software engineering techniques on, but I take pride in my work and all my code is written to the same level of quality as a nuclear reactor! Well, maybe. Good for you. In the meantime, I have a deadline to meet.

So, dear reader, to determine which of these two camps you believe yourself to be in: when was the last time your used mathematically rigorous formal proof techniques to validate your program’s correctness? Yeah, me neither. Well, since we have just found out which camp you are not in, you had better make sure that your code is readable and elegant enough to be able to reason about it intuitively. It’s the only chance you have of delivering reasonably bug-free code at an acceptable level of productivity.

(Oh, and adding some automated testing won’t hurt either way. As Donald Knuth once famously wrote: “Beware of errors in the above code; I have only proved it correct, not tested it.”)