How to think about handling exceptions in Java

Lately I’ve noticed myself sinking a lot of time into trying to understand how exceptions are being handled across our codebase.

I’ll admit that I still find them tricky! I see lots of different approaches to raising, handling and logging exceptions and it’s hard to know which is most appropriate. Exceptions are notorious for obscuring logic and without a sensible consistent approach, they can be hard to get your head around.

So here is me answering lots of my own questions. Note that this is more about mechanics than specific Java syntax.

I’m using a new API. How do I work out what exceptions it might throw?#

A well-written API should include documentation of the exceptions it can throw.

It’s worth taking some time to understand what each exception means and think about how you want to respond to it in the context of your application.

If the documentation is no good, you have a couple of options:

  • Search through the source code to get a sense of the exceptions it might throw
  • Create a quick test class to explore how the API reacts to different inputs
  • Ask whoever wrote the API to provide some better documentation!

Ok, so I understand what exceptions I might encounter now. What do I do?#

There are two fundamental options available to you when you are faced with an exception.

  1. Handle it - this involves writing some code (usually in a catch block) which will cause our application to do something specific if the exception is encountered during runtime

  2. Don’t handle it - this involves letting the exception “bubble up” or “propagate” through each calling method until it reaches the highest level and halts your application

Huh, that second option sounds scary. Shouldn’t I avoid this at all costs?#

Not necessarily. There are lots of exceptions from which your application won’t be able to recover by itself at runtime. For example, if your code is throwing a Null Pointer Exception, you will probably need to redeploy the application with a fix. In these cases, some would argue that you should just let the exception kill the application altogether so it cannot be used.

A more reasonable approach, especially for an application with a user-interface where we don’t want to show some ugly default error in Times New Roman font, is to create a common exception handler at the top level of the application which “handles all un-handled exceptions” - for example by returning a generic error page. You might hear this approach described as “failing gracefully”.

In this case, you probably also want to log something sensible so that you can see why the error page got thrown (more on that in a moment).

So how do I decide whether to handle an exception or not?#

Here’s the rule: You should only handle exceptions which your programme can reasonably be expected to recover from. No more, no less.

You should let everything else “bubble up” to the top level of your application (and consider failing gracefully as described above).

One more note that might help. In Java exception handling, it is generally considered a best practice to throw a checked exception when the caller can be expected to recover from the exception. So if you find yourself up against a checked exception, it’s reasonably likely that you should be able to handle it.

If I’m gonna handle an exception, what are my options?#

Handling might mean logging the exception for awareness but continuing past it because it doesn’t need to stop the application (eg. a batch send email job fails but will run again in 12 hours). Or falling back to a different call which bypasses the exception while still achieving the desired outcome.

And should I do all the handling immediately?#

Bit of a leading question there, so we can talk about “throw early catch late”. Which essentially means that It’s best for an exception to be thrown at the point where an error occurs, because that’s where most is known about why the exception was triggered. But that doesn’t mean that’s where it should be handled.

For example, our code might encounter a low-level exception when trying to open a file that doesn’t exist. The immediate calling code is unlikely to know how to recover from this scenario, but a method further up the call stack might know of another file location to try, for example a default config file.

A good rule of thumb is that an exception should be handled (caught) in the method that knows what to do about it.

I’ve seen exceptions getting caught and thrown as something else, what’s going on there?#

Exceptions caused by low-level parts of your application can be quite specific and other parts of your application might not need to know about them.

For example, imagine your email service asks an email client to send an email via an SMTP server. Unfortunately your email client gets some TechnicalMessagingException from the server. But the email service doesn’t need to know about this specific exception, it just needs to know whether the email was sent successfully or not.

So in our email client, we can rethrow the TechnicalMessagingException as a more generic EmailNotSentException:

try {
    smtpEmailServer.sendEmail();
} catch (TechnicalMessagingException e) {
    throw new EmailNotSentException(e);
}

Notice how we are “wrapping” the TechnicalMessagingException and sending it up to the calling class. This means that our service layer will have access to all the information in the low level exception, without having to look out for it explicitly. It can just catch an EmailNotSentException:

try {
    emailClient.sendEmail();
} catch (EmailNotSentException e) {
    LOGGER.log(Level.SEVERE, "Email could not be sent", e);
}

This log will now include lots of great information about what caused the issue:

Nov 03, 2020 9:31:22 PM EmailExample.EmailService sendEmail SEVERE: Email could not be sent
EmailExample.EmailNotSentException: EmailExample.TechnicalMessagingException: Something really complicated went wrong when calling the SMTP server
	at EmailExample.EmailClient.sendEmail(EmailClient.java:14)
	at EmailExample.EmailService.sendEmail(EmailService.java:16)
Caused by: EmailExample.TechnicalMessagingException: Something really complicated went wrong when calling the SMTP server
	at EmailExample.SmtpServer.sendEmail(SmtpServer.java:6)
	at EmailExample.EmailClient.sendEmail(EmailClient.java:12)
	... 25 more

Of course, one other benefit of wrapping an exception in this way, is that if you decide to use a different SMTP server implementation with a different exception, you will only need to change your code in one place.

And what about logging?#

If you are going to log out an exception, there’s a couple of things to remember:

  1. Log the exception just once. It’s common to see code that “logs and rethrows” but this just leads to duplicate logs; either wrap or log, don’t do both.
  2. Include the full exception. I see a lot of LOGGER.log(Level.SEVERE, e.getMessage()); - while the exception’s message might give you the gist, it’s the stack trace that will help you “tighten the loop” on where an issue is happening.

Useful resources#

I found a couple of resources useful when researching this topic:

  • For all things Java exception handling, WikiWikiWeb has loads of interesting rabbit holes to go down
  • For a nice example on options for handling a recoverable exception, see FileNotFoundException in Java on Baeldung

Thanks for reading!