Simple guide to checked exceptions

While much have been written on checked vs unchecked exceptions, I found very little practical, down to earth, advice on how and why to use them. Here are my thoughts after years of rumination.

While much have been written on checked vs unchecked exceptions, I found very little practical, down to earth, advice on how and why to use them. Here are my thoughts after years of rumination.

The common wisdom

What we are typically told is to use checked exceptions for recoverable conditions and unchecked exceptions for programming errors. For example here[2] or in Effective Java[3]. Don't get me wrong: I do not think this is particularly bad advice, but there are a couple of problems.

First of all, what a recoverable conditions is is vastly subjective. This also gives you the false impression that unchecked exception are not or should be not handled, which is false as we'll see in a minute. But worst of all it does not tell me why! Why should I use checked exception for failures I may expect someone to recover? The horrible answers that I sometime get are in line of "to force the user to think about those cases". And this is a pile of nonsense: the only thing that forces him to do is to put an empty catch block! If he can't be bothered to think about failure modes (for whatever reason) it's not the compiler that is going to change is mind. So, how are checked exception better than, say, just documenting the failure mode? What do I get that I couldn't get otherwise?

Unchecked exception handling

All exceptions are handled, one way or the other. Either you catch them directly, or you pass them up the chain to whatever framework you are using where a default error handling will be used. For example, the default handler in a main method will print out the stack trace and quit; the servlet container will return some error message, and process the next call. Unchecked exception will trigger the default handler automatically.

By default, unchecked exceptions bubble up the stack and are handled by the default mechanism the framework you are using provides.

To be complete: note that there are two kinds of unchecked exceptions in Java: RuntimeExceptions and Errors. Frameworks will typically behave differently: a RuntimeException is usually taken to signal that "this particular operation failed", so you should abort it but may have luck with a different operation; an Error is taken to signal "this failure affected the whole JVM", so you should abort everything, not just the current operation.

Checked exception, instead, do not bubble up the stack automatically. Are there cases where we do not want exception to go up the stack? Where being forced to write:

is actually better? Is actually something we want? That we are grateful for?

The problem with unchecked exception

The problem with unchecked exception is that by default they break encapsulation.

This is true of any exception: to be useful it often has to provide what in that particular implementation failed. But imagine this: you implement some cache for large amount of data, which uses the filesystem to store what does not fit in memory. If you do not catch all the IOException that may be caused by the file access, these will reach the client code. The worst part is that the client may actually start handling those exceptions. If you now switch implementation from files to a database, the error handling is lost.

What you need to do is to define the failure modes of the cache, and then map all the exceptions you get from underneath to those new exceptions. But the problem: you must not let the exception from underneath leak through your abstraction. With unchecked exception you have no way to guarantee that no exceptions are leaking: you have to manually check all the code paths.

Checked exceptions help you control in which layer of your code failures are addressed, and that they do not leak up the stack.

This gives you more control in defining and guaranteeing the proper interfaces for your code. Therefore the following code

The problem with checked exception

The problem with checked exception is that it is the layer below that defines what exceptions the layer above may not want to expose.

This is completely backwards: it should be the layer on top that should declare what exceptions it may not want to leak through. It's a problem because different users of the library may have completely legitimate and contrasting requirements, but they are stuck with one behavior.

This is especially annoying when there is no layer above. If I am writing a script, there is nothing on top; a checked exception does not buy me anything. I may still have to convert all the checked to unchecked just because the default handler may only handles those, even if in this case that is the desired behavior.

IMHO, this shows the tension with checked exceptions, and why some times they are simply in your way. When you want to simply create a prototype, when you are writing unit tests, when you are writing a script: these are all cases where you are not interested in controlling all possible error flows, yet you are paying some price. One may fantasize about ways to solve this, through annotations, other language features. I am merely clarifying the problem.

New rules

Under this light, many things make more sense to me. In practice it does not change much what one one do (it's still pretty close to the common wisdom) but I can put down some rules that I think are more tight and provide more guidance. Keep in mind the bottom line: checked exceptions are there to make sure that failures encountered by a specific library or subsystem are only handled by the correct library or subsystem on top of it. Therefore we can say:

If the error condition can be avoided by correct use of the API, use unchecked exceptions.

The argument here is not that they should be checked too, but it would be too verbose, therefore we allow this exemption; or that the code should not be expected to handle them. The argument is that you are never going to create a layer on top handling all the failures caused by the incorrect use of a library: you fix the original code! For example, NullPointerExceptions are fixed by checking whether the variable is null, not by handling the exception. Same thing goes for all the IllegalArgumentExceptions, IllegalStateException, IndexOutOfBoundsException and the like. You don't want to make sure that they are handled in the layer above, you simply want to help the user find the problem and fix it. The default handling (abort the current operation and report the error) is appropriate in these cases.

If the failure is one that a large number of subsystems can encounter, use unchecked exceptions.

In this case, you do not have a specific set of failure that are generated by a particular library, you can get the failure from anywhere. This means that you cannot write a layer that isolates where such failures are handled, and checked exceptions cannot help you make sure those failures are handled in on place. Take OutOfMemoryException: every object allocation could throw it, it is not even possible to imagine a layer that handles them all without generating others.

Other Errors, such as UnsupportedClassVersionError, AssertionError, ..., fall in this category, and we should point out that they are Errors because after that every piece of code is very likely to be subject to another failure, so the appropriate action is to abort everything, not just the current operation. Again, the problem is not that it is impractical to have them as checked exceptions because they happen everywhere: it is that, because they happen everywhere, we cannot isolate where the problems happen and where they are solved.

A corollary of this is:

If the API does not provide access to a specific subsystem, and is meant to be used at large,
prefer unchecked exceptions which can be avoided by correct use of the API.

If you are writing a library, for example a data structure library like the Java Collections, that is meant to be used anywhere in the code, then, by definition, the same failure could be encountered by any subsystem. Now you would have the error generated by all the uses that potentially merge with each other. The code that handles the failure of the layer underneath may well be using that library too, and we get in a case similar to the above one. Checked exceptions are of little use here because it starts to be less clear who generates what and who handles what.

In this case, it's not that you should use unchecked exception: you should strive to write an API where error conditions can be avoided by correct use. The Collections API, Pattern/Matcher for regular expression, they all use this strategy. With every checked exception, you are implicitly telling your user that he needs a layer on top, which is not acceptable if your API is really of general purpose use.

The remaining cases are failures that are specific to a subsystem, and that cannot be avoided by correct use of the API.
Use checked exceptions for these.

It's as simple as that, really. Now the user will be able to write a layer on top of your API where he can guarantee to himself that the failures are not going to leak. He can implement a database on top of file IO. Another can implement an object broker on top of the database. And they can all trace where the flow of failures starts and stops. These exception defines how this particular subsystem can fail. Which is not all the failures this subsystem can have: it will still have the ones that it shares with all the others, and it has the ones that can be avoided through correct use of the API.

If you are catching a checked exception, and you do not know what you should do with it yet,
wrap it in an UnsupportedFailureException.

I really think such exception should be part of java.lang in the same way that UnsupportedOperationException is: IDEs could generate instead of empty catch blocks, blocks that throw this exception in the same way that they generate method stubs that throws UnsupportedOpertaionException. And when you write your unit test first, it would fail with that exception.

For now, create one in your project and use it everywhere. The reason you do this is obvious: you do not want your client to handle that, because you probably haven't thought exactly what are the consequences of such error anyway. Every subsystem may actually have some failure modes that are unsupported and the correct behavior is the behavior provided by the default handler. And you get documentation for free.

Conclusion

When I explain this to people, they tend to think it's better explanation than what they could find. It makes sense and accounts for the tension there is between the "checked people" and the "unchecked people". In practice it doesn't change much what one should do, except give more understanding of why we do what we do... which may actually be a lot!