After the great brouhaha last week over Joel's essay on exceptions, I feel the need to write an article or two that clarifies, at least to myself, what I think on the subject. That's one of the cool things about weblogs: they're a great excuse for forcing one's self to think through an issue.
Due to time and length constraints, I might have to break this into multiple postings. And you know how reliable I am at that.
Note also, since I do most of my Real Hacking in Java, this will be a very java-centric essay. If you have relevant1 points of view that are provided by other languages, feel free to mention them in comments or trackback.
Myth: "Exceptions force you to handle error conditions, while error codes do not."
Well, it's not really a myth. It's more a misunderstanding, or perhaps just a misstatement of the truth. Anyone who has dealt with the result of casual or inexperienced Java programming will have come across mountains of evidence of exceptions being ignored. For example:
- Exceptions that are just not handled anywhere in the call-stack.
- Exceptions that are caught and swallowed
- Exceptions that are caught, but have no associated error handling more complex than logging the exception's contents and returning from the method.
- Exceptions that are the victims of too-broad catch statements that should not apply to every error condition. (A common problem in Java because unchecked exceptions are subclasses of the checked exceptions)
Some of these things require conscious effort (but very little effort, and they become second nature to programmers who are not taught better), others are common mistakes. Sometimes it may even be the correct thing to do in that circumstance.
A more accurate restatement is to say that "Exceptions change the default behaviour on an error from being unpredictable, to being fail-fast". If an exception occurs that you were not expecting or that your code was not set up to handle, the exception will cause the operation to fail immediately. It will fail without causing any further damage, and without moving the observed error any further from its root cause.
This is valuable. Failing fast is the only valid response to an unexpected error. There's no way forward, because the system is no longer in a predictable state. There's no way back, because without having anticipated the problem, you don't have any way to fix it. So you just have to stop.
This is one reason that returning null from a method is generally a bad idea. null is usually a disguised error code. It means "you expected something to be here, but it really isn't". Worse, in Java a null is a time bomb, waiting to be dereferenced and blow up the code far from the original problem. If there being nothing to return is unexpected, consider throwing an exception. If it is expected, consider a null object refactoring, or changing the method to return an array or collection that can be empty.
You may mistakenly interrupt the fail-fast behaviour using any of the techniques described above. You may deliberately interrupt the failure to handle or correct the error condition. The important thing is that the default behaviour is an improvement over just dropping the problem on the floor.
If your system's operations are isolated and atomic, or if they simply have no side-effects, you can take advantage of the broad control that exceptions give you, by catching all problems, expected or not, at the operation's boundary. At this boundary, any failure that has not already been dealt with can be cleaned up by the generic isolation mechanism. This can be used to clean up after both the unexpected errors, and also those errors you deliberately allow to pass through to the keeper.2
This is usually how we deal with the whole class of errors (usually endemic to application servers) that have no better classification than "Oops, something broke"3. What can the user do if the server runs out of database connections? Bugger-all really. All you can do is catch the exception at the transaction boundary, roll everything back, raise the problem for an operator to deal with, and apologise profusely to the user for being broken.
To be continued (if I'm not sidetracked): flow of control and loose coupling.
1 Relevant as in "relevant to the content of the article", not just relevant to exceptions in general, or why they're superior in one language compared to another.
2 A cricket reference. The "wicket-keeper" is the fielder positioned behind the batsman. Since there are no "strikes" in cricket, when you face a ball that does not threaten the stumps, but that you do not wish to play at, you raise your bat and let the ball pass through to the keeper. In cricket-playing nations, you can signal that you are ignoring something you do not wish to deal with by arranging your hands as if they were holding a cricket bat, and then lifting them backwards and towards your shoulder.
2 On one web application project, the default error page for the system used during development used wording close to "Oops, something went wrong". When it came time to deploy, the customer liked it so much that they asked that it not be replaced with something more professional-sounding.