Checked Exceptions: Good or Bad?
Checked exceptions is a somewhat controversial feature that forces programmers to acknoledge the fact that an exception may be thrown, either by catching it or by explicitly alowing it to propagate up the call stack. The creators of C# chose to leave this feature out, while many Java supporters argue that it's a useful feature contributing to more resiliant software.
Pros
- Fail-Fast Development: Exceptional situations, such as possible IO errors, surface at compile time. Ignoring an unchecked exception becomes a concious decision as opposed to, for example, custom return values indicating errors, or unchecked exceptions.
- Contract: The fact that a method throws a certain exception can be an essential part of the contract. The
throws
declaration is therefore as important as the types of the return value and parameters. Apart from the static analysis it enables, it also serves a documentational purpose for the programmer.
Cons
- Extensibility: You can't add
throws SomeCheckedException
to a method signature without breaking client code, wihch restricts the evolution of APIs. You're obliged to either throw an unchecked exception (even it otherwise has the characteristics of a checked exception) or create a new method and deprecate the original one. - Misuse: Many checked exceptions, including some in the the Java API, should have been implemented as unchecked exceptions. Such misuse leads to frustration and in the end catch blocks that either ignores them or rethrows them as
RuntimeException
s which in turn are never caught. - Propagation: One key feature of exceptions is that they can be thrown at a low level, bubbled up and dealt with at a high level---"throw early, catch late". The issue with checked exceptions is that every intermediate level needs to declare the exception to be thrown. This is particularly problematic when the intermediate levels include interfaces that doesn't allow checked exceptions to be thrown.
This problem arises all the time with callbacks, as the API accepting a callback doesn't know what exceptions the client might want to throw. To give an example, here's a snippet that uses the Stream API:
long calculateBlogDiskSize() {
try {
return blogEntries
.stream()
.mapToLong(e -> Files.size(e.sourceFile))
.sum();
} catch (IOException e) {
throw new BlogSizeCalcException(e);
}
}
This doesn't compile since Files.size
throws IOException
which is not allowed to propagate through ToLongFunction
. It's not unreasonable to view lambdas and the Stream API as alternative control flow structures. Imagine if checked exceptions worked as poorly with for
as it does with forEach
!
Regarding Extensibility
Not being able to add throws SomeCheckedException
to an interface without breaking existing code is both a drawback and a benefit. As mentioned in the beginning, the throws declaration is an essential part of the method contract, and I think most programmers would agree that it would be terrible if the compiler didn't complain about changes to other parts of the contract, such as the return type.
Regarding Misuse
Many people recognize that exceptional situations can be divided into two groups. Barry Ruzek refer to these as Faults and Contingencies:
- Contingencies are out of the ordinary but not unexpected. A user entered a text string when asked for a number, or a configuration file was not found on disk.
- Faults are due to conditions out of the control of the program (someone unplugged the network cable, the computer ran out of memory) or conditions that the programmer was unaware of when writing the code (a method returned
null
while the code expects a string).
Most people stop here, and say that faults should be dealt with by throwing unchecked exceptions and the contingencies with checked exceptions. This causes a lot of frustration and is the reason for a lot the bad reputation attributed to checked exceptions.
There's another dimension to consider as well:
- Unpredictable situations that client code can't forsee or do anything to avoid
- Predictable situations that are statically forseeable and possible for the client to avoid
Putting these together results in the following taxonomy of exceptional situations:
Unpredictable | Predictable | |
---|---|---|
Rare but normal situations due to conditions external to the code, such as a network outage or a file system issue.
|
Situations that are out of the ordinary but not exceptional per se.
|
|
Failures due to external conditions. These are hard for the code to do something about, and in some cases it's even a bad idea to try.
|
Prorgamming errors. These situations are due to bugs and should never have occurred in the first place.
|
Frustration arises when checked exceptions are used to handle predictable control flow. Consider for instance:
try {
int rnd = new Random().nextInt(8);
URL url = new URL("http://mirror" + rnd + ".example.com");
// ...
} catch (MalformedURLException e) {
// Unreachable
}
There's no point in forcing the programmer to wrap this code in a try/catch as a MalformedURLException
will never be thrown during execution. This is an example of what Eric Lippert refers to as vexing exceptions and is a good example of a checked exception that should have been implemented as a runtime exception (if at all as an exception).
The InvocationTargetException
is another example. Why should the programmer be forced to wrap calls to invokeAndWait
in a try/catch in cases where the provided Runnable
clearly never throws anything?
So, checked exceptions are hard get right, and misuse has problematic consequences. It should be noted however, that this is the case with all non-trivial language features. There are many examples of abstract classes that should have been interfaces, and many designs based on inheritance where aggregation would have been better.
Propagation
The fact that each intermediate layer needs to include throws
declarations for checked exceptions to propagate contributes to the verbosity of the language. It's however not very common for checked exceptions to propagate through many layers. They are usually caught and rethrown wrapped in an exception more appropriate for the current level of abstraction.
The fact that checked exceptions don't play well with the standard functional interfaces and the Stream API is definitely problematic. It limits the experience and emphasizes the fact that one should think twice before making an exception checked. If it makes sense to let an exception propagate, it should probably be unchecked.
Conclusion
In liue of other ways to express alternative return modes, such as tupled return values, or out
parameters like in C#, checked exceptions serve an important purpose. Granted a lot of requirements must be met for a checked exception to be a safe bet when designing an API. It should be a situation contingent on the environment, it should makes sense to catch the exception early and it should be easy for a client to recover from it. If this is indeed the case, checked exceptions are a perfect fit.
It's easy to argue both ways for any non-trivial language feature:
- It's hard to get it right, and not getting it right causes serious damage
- It doesn't play well with language feature X
On the other hand,
- It's powerful when used correctly
- You shouldn't ban carpenters from using hammers because some carpenters hit their thumb
In the end one has to weigh the pros and cons and it's hard to make a fully objective call, especially when you're a carpenter who's already proficient with a hammer. Few Java supporters complain about the lack of operator overloading, but things might have been different if Gosling had chosen to include them, programmers had learned to use them responsibly, and the discussion was about removing them.