Wednesday, March 23, 2011

In Defense of Checked Exceptions

I fought in the Checked Exceptions wars, much like your father ...

I have been defending Checked Exceptions since 2004, but lately I have been thinking that it just doesn't fit with where Java is in the computing universe. Ironic since Java is the only language that has them as a compile time constraint (to my knowledge).

Just to reiterate, this is my argument. A followup post will explain why I am walking away from it.

A potentially cogent argument for checked exceptions:

An overly simplistic way of thinking about checked exceptions is that if given the exact same input parameters, the method could sometimes throw an exception, and sometimes not, then that exception should be a checked exception. It tells the user of the API that this is an error condition that they cannot account for ahead of time, prior to calling the method. The programmer writing good code that works needs to realize that his or her application must handle that situation. If by handle that means "crash" then converting to a runtime exception may indeed be appropriate, but that is the clients choice, not the API's.

This is overly simplistic because not all appropriate checked exceptions would fit in this parameter, and there are always grey areas, but this guideline may help develop a reasonable API.

Good examples of such exceptions would be SQLException and ClassNotFoundException. The class being loaded by reflection by definition may not actually be in the classpath at runtime (if you could be sure that it would, you wouldn't need reflection). The database may not be working, or not reachable, at run time.

For operations which always fail given the same input parameters, a runtime exception could well be appropriate (such as ArrayIndexOutOfBounds) but should be documented in the javadocs so that users of the API know what parameters are valid.

One thing which regularly trips up developers, even experienced ones, is that they think they have to handle the "Exception" and not the problem it represents. For example, one of the arguments (expressed here) that checked exceptions are not appropriate is that the caller isn't in a position to deal with the exception.

This is really backwards thinking. When you call a method which relies on a database connection (or other volatile resource such as a file or server connection), you have to know that you may not get what you asked for. Returning null is only appropriate if you could expect to get null legitimately (a value in a column of a database, for example), and the method should be documented to return null. A checked exception means you have to think about what to do about the fact that the method cannot guarantee a consistent result. That is what a checked exception should be telling you: Hey, this may not work out, what do you want to do about that? How do you want to solve the problem when the database went down, or is misconfigured, or whatever?

You should wrap the checked exception when you want to abstract it. For example, if you are writing a datasource API which can flexibly get from a flatfile, a JDBC datasource and/or an XML file, you would want to wrap the various exceptions so that calling programs don't care which one was actually used. But in all cases they are volitile, and may not be available or properly configured at runtime, so the wrapper should be a checked exception.

What to do about exceptions through an interface which cannot throw checked exceptions (Say Comparable, or an ActionListener or the like)? The first thought should be that what the API is telling me is that my operation should be guaranteed. If pressing a button hits the database, then before this method ends, I have to inform the user of failure, and do whatever I wanted to do about that failure, so the event queue can continue on its merry way not caring about the result. Otherwise, the user presses the button, and simply nothing happens. Is that what you want? Comparable is saying: this operation has to work out, with a deterministic result. If it doesn't, it is up to the implementor to figure it out, not the client. It is either greater than, less than, or equal to. There is no option of "I can't figure it out, sometimes." If I always can't figure it out given this input, on the other hand, this is often a ClassCastException, an appropriate use of a runtime exception.

I think this leads to the core problem: Some programmers view checked exceptions as coding problems (how to handle them), not as design considerations (how to construct a program which handles serious potential environment errors in a robust way), and therefore don't handle them properly.

If your answer is that my system assumes that the runtime environment is pristine and never fails, and that level of robustness (or lack thereof) is acceptable to you, then indeed wrap exceptions in a runtime exception. It is a step above swallowing the exception, although probably little better than catching Exception and logging the result. But that is the client's choice, not the API's. The API is telling you something will go wrong which you could not account for programmatically. It is up to you if that bothers you.

In large applications where Exception swallowing becomes the norm, I would bet that that is a symptom of larger problems such as developers producing specific functionality, not well functioning programs, and/or a highly coupled system without clear boundaries of responsibility leaving it unclear when to pass exceptions and when to wrap them, or simply not caring about error handling. Error handling should be as much a requirement as the core functionality. If that requirement is crash early, crash often, then wrapping in runtime exceptions, or even swallowing may be appropriate. At least it is a deliberate decision, and having API's which throw checked exceptions encourage the deliberation, helping good programmers develop good code that works.

No comments: