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.

Checked and Unchecked

Pros

Cons

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:

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:

Putting these together results in the following taxonomy of exceptional situations:

Unpredictable Predictable
Cont.
Rare but normal situations due to conditions external to the code, such as a network outage or a file system issue.
  • FileNotFoundException
  • PrinterException
  • SocketTimeoutException
Situations that are out of the ordinary but not exceptional per se.
  • IllegalArgumentException
  • NumberFormatException
  • MalformedURLException
Fault
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.
  • OutOfMemoryError
  • StackOverflowError
  • NoSuchMethodError
Prorgamming errors. These situations are due to bugs and should never have occurred in the first place.
  • ArrayIndexOutOfBounds
  • NullPointerException
  • ArithmeticException

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:

On the other hand,

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.

Comments

Be the first to comment!