Many applications want to present the errors to their users in a nice human-readable form. This article covers some widely used best practices on this topic.
Before we even start, let’s make it clear:
NEVER expose exception stack traces in your API responses
It’s not only ugly but dangerous from a security point of view. Based on the line numbers in that stack trace, an attacker might find out the libraries and versions you’re using and attack you by exploiting their known vulnerabilities.
Then what to show our users?
Contents
Purpose of Exception Message
The simplest idea is to display to the users the exception message string. For simple applications, this might work, but in medium-large applications, two issues arise.
First, you might want to unit-test that a certain exception was thrown. But asserting message strings will lead to fragile tests, especially when you start concatenating user input to those messages:
throw new IllegalArgumentException("Invalid username: " + username);
Secondly, one might argue that you’re violating Model-View-Controller because you’re mixing diverging concerns in the same code: formatting the user-visible message AND detecting the business error condition.
Best Practice: Use the exception message to report any technical details about the error that might be useful for developers investigating the exception
You can go as far as to catch an exception in a method x()
and re-throw it back wrapped in another exception with a useful message that captures the key debug information from that method x()
. For example, you might include method arguments, any interesting id, current row index, and any other useful debugging information. This is the Catch-rethrow-with-debug-message Pattern:
throw new RuntimeException("For id: " + keyDebugId, e);
You can even instantiate and throwback the same exception type you caught previously. I’m ok as long as you keep the original exception in the chain by passing it to the new exception constructor as its cause
.
This way, the resulting exception stack trace will contain all the debug information you need to analyze the exception, allowing you to avoid breakpoint-debugging. 👍 I’ll explain more about how to avoid breakpoint-based investigation in a future blog post.
At the other extreme, please don’t write 30+ word phrases in your exception messages. They will only distract the reader: they will find themselves staring endlessly at a nice human-friendly phrase in the middle of that difficult code. But honestly, the exception messages are the least important elements when browsing unknown code. Furthermore, the first thing a developer does when debugging an exception is to click through the stack trace. So keep your exception messages short (KISS).
Best Practice Keep your exception messages concise and meaningful. Just like comments.
To sum up, I will generally recommend that you use exception messages to report concise debug information for developers’ eyes. User error messages should be externalized in .properties
files such that you can easily adjust, correct them, and use UTF-8 accents without any pain.
But then how to distinguish between different error causes? In other words, what should be the translation key in that properties file?
Different Exception Subtypes
It might be tempting to start creating multiple exception subtypes, one for each business error in our application, and then distinguish between errors based on the actual throw exception type. Although fun at first, this will rapidly lead to creating hundreds of exception classes in any typical real-world applications. Clearly, that’s unreasonable.
Best Practice: Only create a new exception type
E1
if you need tocatch(E1)
and selectively handle that particular exception type, to work-around or recover from it.
But in most applications, you rarely work-around or recover from exceptions. Instead, you typically terminate the current request by allowing the exception to bubble up to the outer layers.
Error Code Enum
The best solution to distinguish between your non-recoverable error conditions is using an error code. Not the exception message, nor the exception type.
Should that error code be an int
?
If it were, we would then need the Exception Manual at close-by every time we walk through the code. Horrible scenario!
But wait!
Every time in Java the range of values is finite and pre-known, we should always consider using an enum
. Let’s give it a first try:
public class MyException extends RuntimeException {
public enum ErrorCode {
GENERAL,
BAD_CONFIG;
}
private final ErrorCode code;
public MyException(ErrorCode code, Throwable cause) {
super(cause);
this.code = code;
}
public ErrorCode getCode() {
return code;
}
}
And then every time we encounter an issue due to bad configuration, we could throw new MyException(ErrorCode.BAD_CONFIG, e);
.
Unit tests are also more robust when using enum
for Error Codes, compared to matching the String message:
// junit 4.13 or 5:
MyException e = assertThrows(MyException.class, () -> testedMethod(...));
assertEquals(ErrorCode.BAD_CONFIG, e.getErrorCode());
Okay, we sorted out how to distinguish between individual errors, and we mentioned that the user exception message should be externalized in .properties
files. But when and how does this translation happen? Let’s see…
The Global Exception Handler
Providing the team with a global safety net that correctly catches, logs, and reports any unhandled exception should be a key concern for any technical lead.
Developers should never fear to let go a runtime exception.
Let’s write a GlobalExceptionHandler that catches our exception, logs it, and then translates it into a friendly message for our users. UIs typically send HTTP requests, so we’ll assume a REST endpoint in a Spring Boot application, but all other major web frameworks today offer similar functionality (Quarkus, Micronaut, JAXRS).
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final MessageSource messageSource;
public GlobalExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
@ExceptionHandler(MyException.class)
@ResponseStatus // by default returns 500 error code
public String handleMyException(MyException exception, HttpServletRequest request) {
String userMessage = messageSource.getMessage(exception.getCode().name(), null, request.getLocale());
log.error(userMessage, exception);
return userMessage;
}
}
The framework will pass to this @RestControllerAdvice
every MyException
that slips unhandled from any REST endpoint (@RequestMapping
, @GetMapping
…).
At this point, I sometimes faced a debate: where should we translate the error code? On server-side (in Java) or in the browser (in JavaScript)?
Although it’s tempting to throw the problem out on the frontend, translating the error codes on the backend tends to localize better the impact of adding a new error code. Indeed, why should the frontend code need to change when you add or remove a new error?
Plus, you might even write code to check at application startup that all the enum error codes have a corresponding translation in the .properties
files. That’s why I will translate the error on the backend and send user-friendly messages in the HTTP responses.
My Preference: translate the error codes on the backend
In the code above, I used a Spring MessageSource
to look up the user message corresponding to the error code of the caught MyException
. Based on the user Locale, the MessageSource
determines what file to read from, e.g.: messages_RO.properties
for RO, messages_FR.properties
for fr, or defaults to messages.properties
for any other language.
I extracted the user Locale from the Accept-Language header in the HTTP Request. However, in many applications, the user language is read from the user profile instead of the incoming HTTP request.
In src/main/resources/messages.properties
we store the translations:
BAD_CONFIG=Incorrect application configuration
That’s it! Free internationalization for our error messages.
There’s a minor problem left, though. What if we want to report some user input causing the error back to the UI? We need to add parameters to MyException
. After adding other convenience constructors, here’s the complete code of it:
public class MyException extends RuntimeException {
public enum ErrorCode {
GENERAL,
BAD_CONFIG;
}
private final ErrorCode code;
private final Object[] params;
// canonical constructor
public MyException(String message, ErrorCode code, Throwable cause, Object... params) {
super(message, cause);
this.code = code;
this.params = params;
}
// 6 more overloaded constructors for convenience
public ErrorCode getCode() {
return code;
}
public Object[] getParams() {
return params;
}
}
This will allow us to use those parameters in the messages*.properties
files:
BAD_CONFIG=Incorrect application configuration. User info: {0}
Oh, and of course we have to pass those arguments to the MessageSource
when we ask for the translation:
String userMessage = messageSource.getMessage(exception.getCode().name(), exception.getParams(), request.getLocale());
To see the entire app running, check out this commit, run the SpringBoot
class and navigate to http://localhost:8080.
Update: I was asked how to set individual HTTP Response Status Code when reporting different error codes. Please see the comments section, for a detailed explanation. tl;dr: Always use 500 Internal Error.
Reporting General Errors
When should we use the ErrorCode.GENERAL
?
When it comes to what error cases to report to users, many good developers are tempted to distinguish between as many errors they can detect. In other words, they think that their users need to be aware of the reason for every failure. Although putting yourself in the shoes of your users is usually a good practice, in this case, it’s a trap.
You should generally prefer displaying an opaque general-purpose error message, like “Internal Server Error, please check the logs for error id = UUID“. Consider carefully whether the user (NOT developer) can do anything about that error or not. If they are helpless, do not bother to distinguish that particular error cause. If you do, you might have to make sure you continue to detect that error cause from then on. It may be simple today, but who knows in 3 years from now… You’ll end up wondering the $1M question: Is that code really necessary? typical for legacy codebases. It’s a good example of unnecessary and unplanned developer-induced feature creep.
Best Practice: Avoid displaying the exact error cause to your users unless they can do something to fix it
Honestly, if the application configuration is corrupted, can the user really fix that? Probably not. Therefore we should use new MyException(ErrorCode.GENERAL)
or another convenience constructor overload like new MyException()
which does the exact same thing.
We can simplify it even more and directly throw new RuntimeException("debug message?", e)
. Sonar may not be very happy about it, but it’s the simplest possible form.
But what if there’s no additional debug message to add to the new exception? Then your code might look like this:
} catch (SomeException e)
throw new RuntimeException(e);
}
Note that I didn’t log the exception before re-throwing. Doing that is actually an anti-pattern, as I will explain in another blog post.
But look again at the code snippet above. Why in the world would you catch(SomeException)
? If it were a runtime exception, why don’t you let it fly-through? Therefore, SomeException
must be a checked exception, and you’re doing that to convert it into a runtime, invisible exception. If that’s the case, then there’s another widely used option for you: add a @SneakyThrows
annotation from Lombok on that method and let the checked exception propagate invisibly down the call stack. You can read more about this technique in my other blog post.
One last thing: we need to enhance our Global Exception Handler to also catch any other Exception
that slips from a RestController
method, like the infamous NullPointerException
. Let’s add another method to do that:
@ExceptionHandler(Exception.class)
@ResponseStatus
public String handleAnyOtherException(Exception exception, HttpServletRequest request) {
String userMessage = messageSource.getMessage(ErrorCode.GENERAL.name(), null, request.getLocale());
log.error(userMessage, exception);
return userMessage;
}
Note that we used a similar code to the previous @ExceptionHandler
method to read the user error messages from the same .properties
file(s). You can do a bit more polishing around, but these are the main points.
Edit: One more practical advice: when reporting general errors to the user, it is a good practice to add a random generated number (eg UUID or a friendlier form) to the message displayed the UI, telling the user to ‘tell support about this error : ####’. Depending on the complexity and criticallity of your system, you might choose to print such an error code in each error message, or send a JSON error response back to frontend for a more rich display.
You can find the code on this Git Repo.
Conclusions
- Use concise exception messages to convey debugging information for developers.
- Consider using the Catch-rethrow-with-debug-message pattern to display more data in the stack trace
- Only define new exception types in case you selectively catch them.
- Use an error code enum to translate errors to users.
- If needed, add the incorrect user input as exception parameters and use them in your error messages.
- We implemented a Global Exception Handler in Spring to catch, log and translate all exceptions slipped from HTTP requests
If you liked this article, there’s a whole series about exception handling in Java.