The Case Against Checked Exceptions in Java
Checked Exceptions
Checked exceptions are exceptions that extend the Exception class. When one of these exceptions is thrown in code, the compiler checks to see if the exception is handled. This can be done in one of two ways. The first is to handle it in a try block.
try {
// Method that throws IOException (checked exception)
doSomething();
} catch (final Exception e) {
// ...
}
The second is to add a throws clause to the method signature so it can be handled by the caller
public void method() throws IOException {
doSomething();
}
Try Blocks
It is too common for a developer to wrap a checked exception in a try block and have the catch either be empty or log the exception. Where the exception occurs often isn't where it needs to be handled. If it is handled too low in the stack, it can become suppressed. For example, if an exception is thrown at the DAO (Data Access Object) layer for a record that isn't found, if it is handled in the DAO, it will be suppressed. A developer may be tempted to just log the error and try to move on.
User user;
try {
user = userDao.findById(id);
} catch (final RecordNotFoundException e) {
LOG.error("Record not found", e);
}
return user;
Something like this needs to bubble up the stack to the point where it can be reported to the user. Not only this, but in this case, the user will be null. This illustrates introducing a new problem due to being forced to handle the checked exception.
Redundant Logging
If you are going to take a checked exception and convert it to a RuntimeException, it is common to log that exception so the catch isn't empty.
try {
// do something
} catch (final Exception e) {
LOG.error(e);
throw new RuntimeException(e);
}
Where is the RuntimeException handled that is thrown? This can result in another log for the same exception.
Throws Clause
Adding a throws clause to a method signature doesn't seem bad at first, but this can get out of hand quickly. It starts to clutter code, make method signatures complex, and make invoking those methods more complicated. They cascade to each caller and each of those callers until the exception is handled. Consider this simple example:
public void doSomething1() {
// ...
}
public void doSomething2() {
doSomething1();
}
public void doSomething3() {
doSomething2();
}
If doSomething1() changes to have a throws in the signature, that has to be added to each signature in this example since there isn't a try block.
public void doSomething1() throws IOException {
// ...
}
public void doSomething2() throws IOException {
doSomething1();
}
public void doSomething3() throws IOException {
doSomething2();
}
Throws clauses can also make it difficult to do refactors.
Interfaces
Interfaces can have a throws clause on their methods as well. This makes the problem even worse. This is showing an implementation detail on the interface that the implementation may or may not have. Take the java.io.ByteArrayInputStream class. This class implements the java.io.Closeable interface. This interface has a close method with the following signature:
public void close() throws IOException;
If you look at the implementation of this method in the java.io.ByteArrayInputStream, it is the following:
public void close() throws IOException {
}
This requires you to handle an exception when there isn't an exception that is even thrown. If you are adding the throws signature to the method because of this close method, those methods are now stating they throw an exception and they don't.
Lambdas
Lambdas were introduced in Java 8. A lambda's signature is just an interface with a single method. The interfaces Java comes with are in the java.util.function package. These interfaces do not have a throws clause on any of the methods, making them not compatible with functional programming features in Java. If you are trying to use lambdas and you run into a checked exception, you'll have problems. You have to either not use the lambda or create a new interface with the signature that has the throws clause, like the following:
@FunctionalInterface
public interface FunctionWithException<P, R> {
public R apply(final P p) throws IOException;
}
You can also wrap it in a try block and throw a RuntimeException.
items.forEach(
item -> {
try {
process(item);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
)
Third-party APIs
If you are an API (Application Programming Interface) developer and you have to add or change a method's throws clause, when your users upgrade to that version, you will break them. You may have to do this on an interface. One standard for interfaces is that, once released, they are immutable. Do you introduce version 2 of that interface just because of a checked exception, or do you break the user?
Error Handling is an Architectural Problem
Error handling is generally going to be done at your entry point. In a command line program, this would be your main() method. In a web application, it would be the start of the processing of a request. You need the exception to bubble up to the entry point so it can be handled there through some type of error handler. This isn't always the case, but in general, this is more common than not.
Knowing What Exceptions are Thrown
Do you really need to know what exceptions are thrown, and does it apply to how you are using a method? Often times, there isn't anything you can do about an exception, so they aren't handled. Maybe the only thing there is to do is show a message to the client. You'll probably be more concerned about the finally block and cleaning up resources than handling the exception.
Eliminating Checked Exceptions
There are some ways to eliminate checked exceptions while still having those exception types. One tiny library called NoCatch allows you to write code such as the following around a call that has a checked exception, in this case a MalformedURLException.
final URL url = noCatch(
() -> new URL("https://www.bytesize.press");
);
Another interesting way is to take advantage of Java generics. You can trick the compiler with the following:
public static <E extends Exception> void unchecked(
final Exception e) throws E {
throw (E) e;
}
// Notice the missing throws clause
public static void main(final String[] args) {
unchecked(new Exception());
}
Whether or not you should do this is up to your team to decide.
Conclusion
Checked exceptions can be viewed as simply exchanging one problem for another. They do not scale well. Just because something throws an exception doesn't mean you need to know about it. As a team, come to a standard and implement that standard across your project.