Birthing code is not always easy.
Enough puns. Let’s talk about Java exceptions. No matter how hard you try, your code will likely encounter an error and throw an exception (if your language supports exceptions.) It could be anything from unexpected user input to an underlying service outage. An exception will be thrown and it’s important to do something useful with it. That doesn’t mean putting try-catch blocks everywhere or trying to recover everything, in-fact I’ll argue the opposite in a few situations.
This post introduces a few common issues I’ve seen when working with Java code-bases and developers that lead to poor debuggability or other operational pains.
Practices
Stop the catch-log-throw shuffle
This is a common problem in Java code that I see. At multiple levels, developers will catch broad exceptions because there are checked exceptions just to wrap an re-throw or to log to the console.
|
|
Then this happens at every level in the call stack and soon enough your error log is 10 screens high of call stacks and the exception class has lost all meaning.
Avoid needless wrapping of exceptions. Every wrapped exception adds more noise to understanding the problem. Each wrapped exceptions should provide some additional context or an abstracted exception type.
Take the above example:
- The outer RuntimeException provides no additional context and the exception class is not specific at all. Make the error clear and direct. Is it important to know which item failed to update?
- The log statement probably happens at every single catch block. The application log is probably full of useless log statements. Instead, log only at the high point in you call stack.
Logging with Log4j/Slf4j correctly
The following example breaks down a common way developers catch and log exceptions in code. Can you spot the issue? Hint: it has to do with the log format.
|
|
Running the above code gets the following log statement. Where’s the call stack or the nested exception information?
|
|
As it turns out, Log4j has special logic when it’s formatting the log statement. If there are n placeholders and n+1 arguments and the last value extends from Throwable, then it prints out the exception call stack and nested exceptions. If there are n placeholders and n arguments, it doesn’t matter if the last argument is a Throwable, it calls #toString() on all the objects.
The one edge case to this is if you call log.error(“msg”, throwable), then the compiler directly calls Logger#error(String,Throwable), but the end result is the same.
Instead, don’t include a log placeholder for the exception and always pass it as the last parameter:
|
|
When we run the above code, we now get a much more intuitive log statement:
|
|
If you’re using IntelliJ, you can automatically catch issues like this. It’s won’t detect all issues, but it’s a good start.
|
|
Find it in: IntelliJ Inspections: Java | Logging | Number of placeholders does not match number of arguments in logging call.
Include Inner Exceptions
Almost always include the inner exception when throwing an exception in a catch block. Unless you’re careful to include sufficient to explain what happened, you’re more likely to throw an exception that contains not enough information. Special care should be taken when exposing exceptions outside a security boundary, such as to a caller of a service. Log the full details, but then truncate to a minimal amount of information in the response body.
For example, take this:
|
|
With this, the WebApplicationException does not have an inner exception, so consumers have no context about what happened. In this specific case, it’s critical for the caller to know what argument was invalid so they can fix it.
Include an inner exception when wrapping the exception:
|
|
Don’t accidentally mistake developer mistakes for validation issues
NullPointerExceptions are commonly thrown by validation functions like Lombok’s @NonNull or Guava’s Preconditions. They’re also frequently caused by developer mistakes when you call a method on a null. Unfortunately this can lead to poor exception handling if you assume that NPEs are only thrown by your validation code.
Take the following example. NullPointerExceptions can be thrown in any line of code here. It could mean that the variable something is null and it’s possible handle it or it could mean a developer made a mistake (as I did) and tried to call a method on a null object.
|
|
Extract out exception message construction
If you create and throw custom exceptions, you might find yourself writing code like this:
|
|
If you throw this exception multiple times, then you may end up copying and pasting the String.format code in multiple places. Instead just move the message construction onto the exception class itself:
|
|
Structured exceptions
Pretty much every exception I see thrown converts all the problem details into a single giant string message. Strings are terrible at encoding information. Sure a human can read it, but often times code needs to analyze it. Say you’ve got a library that throws a throttling exception when the client calls it too frequently:
|
|
As a caller, I might want to know when I actually can try again. Since this is currently in a string, I’d have to string parse this exception message to find out. That’s horrifying.
Instead, have your exception object property getters for these different facts:
|
|
RFC9457 is a good resource on how to apply this style of error structuring to an HTTP endpoint.
Checked vs Unchecked Exception
Checked exceptions are exceptions which are checked by the compiler, if they are being explicity thrown or caught. The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions. Checked exceptions need to be declared in a method or constructor’s throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.
Examples of checked exceptions that we might have seen are IOException, JSONParseException.
Every exception which is subclass of the RuntimeException class is an unchecked exception and not checked by the compiler. A common example of this is the NullPointerException.
Which type of exception should we use when creating our own exceptions?
Almost always the recommendation is to create an unchecked exception.
Checked exceptions, if not modeled correctly, adds unnecessary burden on the developers. If you throw a checked exception in your code and it can only be appropriately handled, say 3 - 4 layers up, it has to be added in each method’s declaration. Imagine adding/removing a checked exception in the code which would require layers of code to be refactored. Checked exceptions also do not add any value in our service interfaces as the actual clients will never catch it.
The worst offender for useless checked exception is Charset#ofName which throws a checked IllegalCharsetNameException. Every time I’ve used it, I’ve always passed the static string UTF-8
and if it’s missing, I’m screwed.
Imagine, every time you have to deserialize or serialize a file you end up with this the following code and this untestable catch block that reduces my code coverage.
|
|