Exception Handling Guide in Java

This article presents a pragmatic, clean approach to working with exceptions in the Java applications we build today.

If you want a live walk through the concepts in this article and the others related to exception handling, here is a talk I gave on this topic at the Java Champions Conference in 2021:

https://www.youtube.com/watch?v=LRwCE7GreSM

A Bit of History

At first, there were no exceptions.

Forty years ago, when a C function wanted to signal an abnormal termination, it would return a -1 or NULL. But if the caller forgot to check the returned value, the error would go unnoticed.

In 1985, C++ tried to fix that by introducing exceptions that propagate down the call stack and, unless caught, terminate the program execution. However, developers suddenly were facing a new challenge: how would the caller know what exceptions are thrown by the function they called?

Ten years later, Java was born. Since all developers back then were using C/C++, one goal of Java was to attract them with features like Garbage Collection, safe references (instead of pointers), smooth build process, unified source code (instead of .h/.cpp), portability, and others. At the same time, Gosling, the father of Java, tried to fix the problem of mysterious exceptions by forcing developers to become aware of checked exceptions thrown by the functions they called. Of course, some errors couldn’t be foreseen (like ArrayOutOfBoundsException or NullPointerException), so these remained invisible runtime exceptions.

You probably already know that checked exceptions should be used only for "recoverable errors". Only then it pays to force your caller to try-catch or throws your exception.

But all this was happening in The Age of Libraries. In the ‘90s, developers realized the importance of reuse: libraries and frameworks were being created everywhere. The truth is that when writing a library, it’s very tempting to consider that the code using it might be able to recover from a wide range of errors, especially if you don’t have a very clear picture of how your clients will use it. Thus, the urge towards checked exceptions: let’s notify our callers of everything,... as they might be able to recover.

Furthermore, back in those days, many applications implemented UI or batches, and these typically need to handle the exceptions to prevent them from terminating the program.

But that age is gone.

In the applications we write today, we rarely recover from exceptions. Instead, we terminate the execution of the current use-case ASAP. In a web application, we would return a 400/500 HTTP status code. When handling messages from queues, we might reject that message. And in a batch, we would probably skip or report that line with (eg) Spring Batch’s assistance. We rarely recover from an error anymore.

file

Disclaimer: This article is about building custom applications, like almost all of my client companies are doing. It is only partially applicable to writing framework/library code.

Sadly, 25 years later, checked exceptions are still here.

Only use Runtime Exceptions

Imagine the following (overly?)simplified configuration loader:

class Config {
   public static Date getLastPromoDate() throws ParseException, IOException {
     Properties props = new Properties();
     try (Reader reader = new FileReader("config.properties")) {
        props.load(reader);
     }
     SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
     return format.parse(props.getProperty("last.promo.date"));
   }
}

(Disclaimer: config files shouldn’t be read repeatedly but cached somehow; this is just a simplified example to illustrate some standard exceptions occurring.)

And here is the business logic code using our Config class:

public void applyDiscount(Order order, Customer customer) {
 try {
   if (order.getOfferDate().before(Config.getLastPromoDate())) {
      System.out.println("APPLYING DISCOUNT");
      order.setPrice(order.getPrice() * (100 - 2 * customer.getMemberCard().getFidelityDiscount()) / 100);
   } else {
      System.out.println("NO DISCOUNT");
   }
 } catch (Exception e) {
   // TODO
 }
}

The Diaper Pattern

Have you ever encountered an empty catch block? One in every 10 developers attending my training sessions found this in production code at some point in their career. This terrible mistake even got a name:

The Diaper Anti-Pattern = catching all exceptions and just silencing them.

I would name it "shawarma-style error handling" when explaining it to younger developers for two reasons. First, you don't know precisely what exception you swallow. Secondly, shawarma-maker becomes a possible future career path if you regularly apply this technique (I’m evil here, right?).

But let me explain why.

In production, when the applyDiscount() method above got called, it abruptly terminated for some orders without printing neither APPLYING DISCOUNT, nor NO DISCOUNT. Can you figure out what happened for those orders?

Yup... a NullPointerException occurred in the if condition for the Orders having a null offer date, but then the exception was hidden by the Diaper Pattern. You can experience it yourself by running the InProduction class from this commit. By the way, I wrote another blog post about how to avoid NullPointerException, using Optional and a self-validating data model.

But the real-world code is never this simple, and you can waste hours to find such a swallowed exception in typical production code. You will probably have to breakpoint around, as there's nothing in the logs. This is bad, very, very bad.

But how did the code end up like that?

First of all, you can selectively catch multiple exceptions using the syntax catch (ParseException | IOException) only since Java 7, released in 2011. For a large successful Java application, ten years is not so long ago, so the author of the code might have caught Exception because he didn't want to repeat an identical catch block (one for ParseException and one for IOException).

Okay, but still, why is the catch block empty ?!?

Eh... that's another story. Three years ago, they might have sent an email to ask how to handle that exception, or they might have just rushed and forgotten to handle it. Or perhaps one of the exceptions they caught there was regularly popping, polluting the log every minute.

In the case of repeating exceptions, I would log that exception on the TRACE level, as you can decrease the log level in production without a restart if needed, via Spring Actuator or via a Logback JMX Bean. But before doing that, I would first try to prevent that exception from occurring by doing some pre-checks. If that’s not possible, I will make sure I catch and log.trace that exception exactly around the line throwing it. Be very cautious with ignoring exceptions or logging them at a low log level!

You would never swallow exceptions, I know.

As a matter of fact, even the catch block auto-generated by Eclipse or IntelliJ would .printStackTrace() by default, so you’re safe, right?

By the way, here’s a fun fact for you: .printStackTrace() prints the exception to System.err, not System.out, and by default, most frameworks (Spring Boot included) don't capture System.err to the log file unless you take explicit steps. So despite e.printStackTrace(), you will probably won't see anything in your log file. Oops!

But let’s come back to our beloved NullPointerException. By the way, do you know that songs were written for this exception? (and other hard to trace exceptions in general)

But here it is, the exception:

   C:\workspace\openjdk-15_windows-x64_bin\jdk-15\bin\java.exe ... victor.training.exceptions.InProduction
   java.lang.NullPointerException: Cannot invoke "java.util.Date.before(java.util.Date)" because the return value of "victor.training.exceptions.model.Order.getOfferDate()" is null
       at victor.training.exceptions.Biz.applyDiscount(Biz.java:20)
       at victor.training.exceptions.InProduction.main(InProduction.java:13)

Let's take some time to enjoy the nice message reported from Java 15 onwards for NullPointerExceptions. You could be an absolute rookie and still get where the problem is. Up to Java 11 LTS however, the NPE had a very opaque message:

   C:\Users\victo\.jdks\corretto-11.0.7\bin\java.exe ... victor.training.exceptions.InProduction
   java.lang.NullPointerException
       at victor.training.exceptions.Biz.applyDiscount(Biz.java:20)
       at victor.training.exceptions.InProduction.main(InProduction.java:13)

Smooth!

So, how should we handle those nasty checked exceptions?

By rethrowing them wrapped in a new RuntimeException(checkedException), of course.

Never decapitate your exceptions: always set the cause of the rethrown exception to the original exception, whenever there is one.

file

But where should we convert checked exceptions into runtime ones? Inside Config.getLastPromoDate() or within Biz.applyDiscount()? To understand the right answer, let’s talk a bit about abstractions.

Checked Exceptions are an Abstraction Leak

An abstraction is a simplification of reality, attempting to hide the (complex) implementation details. From a design perspective, a method like getLastPromoDate() throws IOException, is an abstraction leak because it reveals the implementation detail that it’s working with files. Why should the caller know, or care, that there are files or any other sort of I/O involved?

From the same perspective, you should avoid adding implementation details to your method signatures. You should generally avoid returning Files, ResultSet, or foreign data structures or taking them as parameters. Of course, there are exceptions to this rule, but I will dive more in-depth on this in a later blog post about the Adapter and Dependency Inversion Principle.

In our case getLastPromoDate() , that's why we should NOT declare any checked exceptions but instead wrap the checked exceptions in a RuntimeException and throw that instead, to hide the implementation detail that it's using files and parsing dates. Here's the new code (commit):

   public static Date getLastPromoDate() {
     try{
        Properties props = new Properties();
        try (FileReader reader = new FileReader("config.properties")) {
           props.load(reader);
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        return format.parse(props.getProperty("last.promo.date"));
     } catch (ParseException | IOException e) {
        throw new RuntimeException(e);
     }
   }

How to handle Checked Exceptions?

There are two main reasons why we only allow runtime exceptions through our central code today:

  1. Avoid abstraction leak and
  2. Reduce the change of Diaper Anti-Pattern, since developers don’t have to try/catch/throws exceptions

We should catch any checked exception attempting to enter our core logic and rethrow it wrapped in a runtime exception. We should then handle this new exception at the end of the workflow, far away, in a top-level global exception handler.

Best Practice: Do not propagate checked exceptions through your core logic.

Many years after this became the widely accepted best practice, many developers were still suffering from Post-Traumatic Stress after having spent too many nights hunting for swallowed exceptions. Those people developed another bad habit, on the other extreme:

} catch (ParseException | IOException e) {
    log.error(e.getMessage(), e);
    throw new RuntimeException(e);
}

The Log-Rethrow Anti-Pattern: “I’ll rethrow it, okay, but let me just log it here anyway, just in case” said the elder.

Why is this a bad practice? Because printing and throwing back the exception will log the error multiple times, turning your log into a frustrating, unreadable pile of garbage. Try it yourself: commit. All this because the developers are afraid that the exception might slip out without being logged anywhere.

The real solution is to have a global exception handler put in place at every exit point that safely logs any unhandled exception. You can read more about how to implement such a global exception handler for REST endpoints in my previous blog post on this topic.

Best Practice: make sure any unhandled exceptions are logged, in whatever thread they might occur.

The 'thread' part above is the challenge because, besides HTTP endpoints, our code might run on other threads like thread pool executors, CompletableFuture, @Async, @Scheduled, message endpoints, or others. Take careful measures to log exceptions on whatever thread they occur.

Your developers should feel safe that exceptions are never lost.

There is almost no reason left to add a catch within your core domain logic if you do this. Unless you're retrying or working around an error condition, but you almost never do that.

Reasons to Catch-Rethrow

There are four reasons to catch an exception and rethrow it wrapped in a new one:

  1. Report a particular user-friendly message
  2. Unit-Test a thrown exception
  3. Report debugging information via new exception message
  4. Get rid of a Checked Exception, when none of the above applies

We covered the first two points in this article, emphasizing the benefit of relying on enum error codes to distinguish between interesting error conditions.

For (4) you can consider using @SneakyThrows from Lombok, as explained in this article, or Unchecked from jOOL in case you want to handle checked exceptions when working with Streams.

This leaves us to focus on (3) in the following.

Have you ever had to use breakpoints to debug an occurring exception? If you did, then here's a trick you can use to avoid doing that: the Catch-Rethrow-with-debug-message pattern. You catch the exception in a method having some interesting details that you’d like to inspect when debugging it, and rethrow the exception wrapped in a new exception with a message that captures those details:

    long interestingId;
    ...
} catch (RuntimeException e) {
    throw new RuntimeException("Failed for " + interestingId, e);
}

When the exception stack trace unfolds at the end, you'll see the interestingId nicely laying in the log file, enabling a cold, relaxed analysis.

Use exception messages to convey debugging information for developers.

Conclusions

  • Avoid Diaper Anti-Pattern: don’t ever swallow exceptions
  • If you have to catch exceptions, catch them as close as possible to their origin
  • Only allow Runtime Exceptions to pass through the core logic of your application
  • Consider @SneakyThrows and Unchecked to get rid of checked exceptions
  • Avoid Log-Rethrow Anti-Pattern: set up and trust the global exception handler
  • Use exception message to convey debugging information todevelopers

More Tips

  • Don’t throw from a finally block: the new exception might hide a preexisting exception
  • Throw before mutating an object state: leave the objects’ state unaltered
  • Don’t use exceptions for normal flow control: they are expensive, and they can smell like a GOTO instruction

Further Reading

Popular Posts

2 Comments

  1. Nice article and even better talk, thanks!
    What about “async exceptions” mentioned in the further reading section. Is that a future blog post of yours?
    I’m very curious about that one! Will it cover resource cleanup, i.e. async `finally`?

    1. I didn’t write anything on that topic, sinc it’s a niche and also depends on the underlying platform: Spring’s/Micronaut @Async, JDK Completable Futures, Reactor, etc…

      However, a very good starting point is the deep dive by Venkat Subramaniam: https://www.youtube.com/watch?v=0hQvWIdwnw4
      Indeed, there are equivalents for all ‘traditional’/imperative patterns of error handling, and here is a list of how for example Reactor translates them:
      https://projectreactor.io/docs/core/release/reference/#_error_handling_operators

Leave a Reply

Your email address will not be published. Required fields are marked *