Logging exceptions in Java
Logging is an essential component of any application, especially when a critical error occurs. Whether or not you recover from an exception, logging the problem can help you identify the cause of – and ultimately the solution to – potential problems in your application.
In this post, we’ll look at common approaches to Java exception handling and logging. The examples use Log4j2, but the concepts apply to almost all modern logging frameworks.
When to Log Java Exceptions
One of the first questions that often comes up when logging exceptions is: Where should I put the logging code? Logging can take place in the method where the exception occurred, or it can follow the exception up the call stack until it’s eventually handled by one of the calling methods. Choosing one option over the other depends mostly on how your application is structured. In either case, the logger will need enough information to record the cause of the error.
Log at the Source
Logging close to the source of the error provides as much detail about the error as possible. For example, imagine we have a program that writes a string to a file. In our program, we have a class that opens a file, writes to it, and then closes it. In this class, we catch and log any IOExceptions:
While this approach lets us create specific log messages, it limits the available context. We have access to the stack trace, but we don’t know what particular course of action triggered the exception. Logging close to the source also adds complexity to the application, since it requires each component to provide its own logging implementation. This can be mitigated with frameworks such as SLF4J, but it still ends up adding redundancy and overhead.
Log Further up the Stack
Rather than log directly at the source, we can pass the exception up the stack and log it at a point where we have more contextual information. This can take place in the middle of the call stack, or even by an umbrella handler at the top. This can even be done using Thread.setDefaultUncaughtExceptionHandler(), which lets you specify an exception handler for uncaught exceptions for the entire application. Building off of the previous example, we can set the MyFileWriter class to throw an IOException, which we’ll catch in WriterClass.
With this approach, logging becomes more centralized and fewer components need to implement their own logging. The downside is that some of the granular information provided by the bottom-level component is lost. Fortunately, we still have access to the stack trace, which doesn’t change no matter how many times the exception is rethrown.
Linking Separate Log Events
Regardless of which approach you take, having the ability to associate multiple log events can greatly improve your ability to track down the cause of an exception. Imagine our WriterClass component is part of a large multi-user service. When it fails, we need to know which user triggered the exception and what they were doing when the exception was triggered. We can associate each event with a unique identifier – such as a user ID – that persists throughout the lifetime of the component.
Log4j2 introduced the concept of the Thread Context, which is built off of the Mapped and Nested Diagnostic Contexts used by Logback and other frameworks. As the name implies, the Thread Context is a thread-wide repository of related data. This data can be accessed by the logger to associate unique information with a log event, such as a user ID or session ID. This way, you can relate log events even if they originate from different sources or at different times.
Using the Thread Context Map, we can associate values with keys using the static
%X conversion pattern (or by using a structured layout), you can add values from the Thread Context into your logs. Using a log management service such as Loggly or even a command line tool such as grep, you can then search and sort entries for this particular user by matching the user ID.
What to Avoid When Logging Exceptions
The following tips are geared towards exception handling in general, but they can also prevent problems when logging.
Don’t Log and Throw
No matter which approach you take when handling exceptions, log the exception at the same time you handle the exception. If you log the exception and then throw it, there’s a chance that the exception will be logged again further up the call stack, resulting in two log events for the same error.
To prevent confusion, log once and log consistently.
Don’t Catch and Drop
A surprisingly common approach to exception handling is to catch the exception, then ignore it. Not only is the issue not handled, but we have no record of a problem ever occurring.
As a bare minimum, add a logging statement or display an alert to notify the user that an error occurred. Ideally you should log the stack trace provided by the exception, or throw the exception and log the event further up the stack.
Don’t Log Too Little
Another common (and misleading) practice is to log only a portion of the exception. While this does provide some details about the error, it usually results in the loss of more useful information. For example, this statement creates a log entry with a generic message followed by a very brief description of the exception:
With Log4j2 and other logging frameworks, you can pass the exception as a parameter to the logging method. By default, Log4j2 logs the exception’s stack trace along with the message provided. By extending Log4j2’s PatternLayout, or by switching to a more structured layout, you can include more detailed information about the stack trace.
The key is to provide enough information for a developer to discover what went wrong, where it went wrong, and why it went wrong.
Don’t Catch General Java Exceptions
Catching the Exception class is like using a trawling net to catch a single fish. Not only does it make it more difficult to handle the exception in a specific way, but your program could end up catching exceptions it was never designed to handle.
Generally speaking, you should only catch exceptions that you can handle or delegate to the appropriate handler. Instead of using a catch-all, catch only the specific exception types that your code is likely to throw. You can do this by implementing separate catch blocks, each with their own exception-handling logic:
Starting with Java 7, you can catch multiple exception types in a single block by separating the exception type with a vertical bar ( | ).
While it’s still recommended to catch unique exceptions per catch block, this lets you simplify your code.
You should also consider using the
Thread.setDefaultUncaughtExceptionHandler() method. As mentioned earlier in Log Further Up the Stack,
setDefaultUncaughtExceptionHandler() can be used to log the event or perform a final action even if the error is unrecoverable.
Other Tips and Techniques
This post only covers a small subset of logging and exception-handling methods for Java. If you have any recommendations, feel free to leave them in the comments below.
Andre Newman is a software developer and writer on all things tech. With over eight years of experience in Java, .NET, C++, and Python, Andre has developed software for remote systems management, online retail, and real-time web applications. He currently manages 8-bit Buddhism, a blog that blends technology with spirituality.