Philipp Hauer's Blog

Engineering Management, Java Ecosystem, Kotlin, Sociology of Software Development

Checked Exceptions are Evil

Posted on Mar 28, 2015. Updated on Jun 12, 2022

Java has checked exceptions and is out on a limb. Is there a reason, why other languages like C++, Objective-C, C#, Kotlin, Scala don’t support this concept? What is the problem about checked exceptions and what can we do instead? And most important: What do water wings and checked exceptions have in common? This article gives the answer to all of these questions and shows why unchecked exceptions are the better choice.

The Root of all Evil: Enforced Handling

Consider the following snippet:

private String getContent(Path targetFile) {
    byte[] content = Files.readAllBytes(targetFile);//compile error: "Unhandled IOException"
    return new String(content);
}

This code doesn’t compile, since Files.readAllBytes() throws a checked exception. We are forced to handle this IOException. That’s annoying, but what can we do?

  • Option A: Rethrow the exception
  • Option B: Handle the exception

Option A: Rethrow the Exception

private String getContent(Path targetFile) throws IOException {
    byte[] content = Files.readAllBytes(targetFile);
    return new String(content);
}

This makes the compiler happy. But are we happy now? Actually no, since we can easily run into troubles with rethrowing.

Checked Exceptions are Part of the Signature

Consider the following method call stack.

Cascading Signature Changes

Cascading Signature Changes

If we throw an IOException in UserRepository.getContent(Path) and want to handle them in ContextMenu.menuClicked(), we have to change all method signatures up to this point. What happens, if we later want to add a new exception, change the exception or remove them completely? Yes, we have to change all signatures. Hence, all clients using our methods will break. Moreover, if you use an interface of a library (e.g. Action.execute(), Java 8 Stream API) you are not able to change the signature at all.

Exposing Implementation Details in the Signature

Consider the method signature from the above example:

UserRepository.getUsers(Path) throws IOException

Assume that UserRepository is an interface. Will the various implementations of UserRepository throw an IOException like our file system based implementation? Not at all. A database implementation might throw a SqlException or a WebService might throw a TimeoutException. The point is that the type of exceptions (and whether they are thrown at all) is specific to the implementation.

But the client is forced to handle the IOException although he knows that this exception will not occur in the database implementation he uses. Moreover, the client doesn’t care if an IOException or a SqlException is thrown. He just wants to know, if something went wrong. Consequently the thrown exception should be implementation agnostic.

Option B: Handle the Exception

Ok, let’s handle the exception! We’re going to handle the exception – by… well… how?

private String getContent(Path targetFile) {
  try {
    byte[] content = Files.readAllBytes(targetFile);
    return new String(content);
  } catch (IOException e) {
    //What to do?   
  }
}

No Appropriate Handling Possible at this Point

That’s the point! What to do with the IOException in this low-level method? We can’t appropriately handle them here. Maybe it could be handled at a higher level by giving the user a feedback in the UI, but that’s nothing we should do within this method. Nevertheless, the compiler enforces us to handle it here.

No Recovery Possible at all

A common “handling” of a checked exception is to log the error and to abort (the current operation or the whole application if we can’t continue). Log and Exit. That’s ok. But why we don’t let the exception fly through the whole stack trace, which would have the same effect? If we can’t recover from this exception, why are we forced to handle them? The enforced handling makes no sense at all if we want to exit the operation anyway in case of an exception.

Further Issues

Checked exceptions leads to annoying boilerplate code (try {} catch () {}). Every time you call a method that throws a checked exception, you have to write the try-catch-statement.

The compiler forces us to catch the exception. Often this ends up in a mixing of main logic and error handling. But these are separate concerns that should be handled separately and in a coherent way. The multiple try-catch-statements let your error handling scatter over your whole code base.

There is the danger that we simply forget to implement the catch block, because we didn’t really know what to do or assume that this exception will never occur. Or we just want to quickly test a method call throwing an exception and plan to implement the catch block later, but forget it. An empty catch block swallows the exception. We will never be informed when the exception occurs. The application will behave incorrectly and we have no clue why. Happy debugging.

And finally, a lot of checked exceptions are technical (like IOException) and don’t provide us with semantics regarding to the domain.

Java 8 Lambdas and Streams

Checked exceptions are extremely annoying when it comes to lambdas and streams. Let’s assume we want to call a method in our lambda that throws an IOException.

List<Path> files = // ...
List<String> contents = files.stream()
       .map(file -> {
           try {
               return new String(Files.readAllBytes(file));
           } catch (IOException e) {
               // Well, we can't reasonably handle the exception here...
           }
       })
       .collect(Collectors.toList());

Since the IOException is not declared in the functional interface for the lambda (java.util.Function.apply() ), we have to catch and handle them within the lambda. This try-catch-boilerplate blows up our code and makes it less readable.

The Solution: Use Unchecked Exceptions And Wrap Checked Exceptions

If you define your own exception, always use unchecked exceptions (RuntimeException). If you have to handle a checked exception, wrap them in your own domain/high-level exception and rethrow them. In our getContent() method we can throw our own RepositoryException that extends RuntimeException.

private String getContent(Path targetFile) {
  try {
    byte[] content = Files.readAllBytes(targetFile);
    return new String(content);
  } catch (IOException e) {
    throw new RepositoryException("Unable to read file content. File: " + targetFile, e);
  }
}

public class RepositoryException extends RuntimeException {
//...
}

Now the client of getContent() (or getUsers()) has the freedom to handle the RepositoryException at the point where it is most appropriate. The client decides, not the compiler.  He can let the exception fly up to a central point where all exceptions are handled and, for instance, provide feedback to the user.

No cascading signature changes with unchecked exceptions

No cascading signature changes with unchecked exceptions

Is there a possible recovery for the RepositoryException? Fine, then do it. If not, no problem, either you catch it and provide a feedback to the user or you don’t catch and exit the operation this way.

It’s true, that this approach demand more discipline. You should not forget to catch the RepositoryException if you want to handle it. Moreover, you have to document that your method throws a RepositoryException. The compiler doesn’t take you by the hand any longer. You have to take care about the exception handling. But you are much more flexible and relieved from the boilerplate code.

Documentation of Unchecked Exceptions

But how do I know, that a method throws an unchecked exception? The compiler doesn’t tell me. Yes, you are responsible to document unchecked exceptions carefully. There are two options:

a) Use JavaDoc

/**
 * @throws RepositoryException if ...
 */
private String getContent(Path targetFile) {

b) Use the throws clause

private String getContent(Path targetFile) throws RepositoryException {

Some people state that JavaDoc should be used for unchecked exceptions and the throws clause only for checked exceptions. However, I don’t agree with this dogmatic approach. I’m more pragmatic in this point: I personally always use the throws clause, simply because you get better tooling support depending on your IDE (e.g. code completion when writing the exception in the catch block, highlighting of the throwing method if you select the exception in the catch block or vice versa). Moreover, you can still use JavaDoc additionally for further documentation about the exception.

Summary

Use unchecked exceptions. They enable freedom and flexibility. _We _decide on our own, whether and where we want to handle exceptions. Not the compiler. But this advantage demands more responsibility.

“With great power comes great responsibility”

There is still one question left. What do water wings and checked exceptions have in common?

At the beginning you feel safer with them, but later they prevent you from swimming quickly.