Presenting Exceptions to Users

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 to catch(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 MessageSourcewhen 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.

Popular Posts

5 Comments

  1. I’m really liking this series, Victor, and I’m really looking forward to your next talk on exceptions in Java.
    Also I couldn’t agree more on the one-exception-to-rule-them-all guideline. Our codebase is littered with dozens of them and they’re always handled the same:
    1. Catch it in the @ExceptionHandler
    2. Log it
    3. Translate it into the appropriate HTTP error code (BAD_REQUEST, NOT_FOUND, etc.)
    The funny thing is that we ALSO have an enum error code to tell them apart. You know, the double check xD
    I’d like to know, using only one exception type and multiple enum values, how you would proceed with point 3. Would you have a rather long switch statement in the global exception handler or would you bake the HTTP error code into the exception type (ugly) or in the ErrorDTO?

    1. Well, I would first seriously challenge the usefulness of the http error code. Leaving aside “the dogma” of the HTTP protocol, does anyone really cares about that http status code? Does your client really looks at the code or it handles generically any ‘error’ code (like all major JS frameworks do)?

      1) If you’re sending a user-friendly translated message to the UI, then the precise code is probably not relevant.
      2) On the other end, if your client is another application, then you probably want to report precise error codes in the json body of the HTTP response. like {“code”:613, “message”:”readable..”}. Then again the HTTP response status code is typically not that important.
      3) If however, the API you’re building is exposed to unknown clients over Internet or a large intranet, then it pays to try to comply more with the HTTP protocol and start distinguishing between 400 (Client Input) and 500 (my bad).

      I never was in the position to have to choose 3 in the 7 projects I lead, but most internet companies do have some apps exposed ‘in the wild’. (Wild Wild West = WWW)

      But I’ll assume however that you really want distinct ERROR CODES. To answer your question: I would define additional entries in the message.properties files, like:
      GENERAL.code=500
      BAD_CONFIG.code=400

      and then fetch them the same way I fetched the user message. Oh, and of course, I would have a default of 500 probably and the xyz.code key would be optional. In other words, needed only if you want to return a non-500.

      Thanks for the question!

  2. Thank you for the article. I want to add few notices to this theme.
    1. I use HTTP codes and custom exceptions. Our react client handles all HTTP exceptions and processes them. The client(React, Android, Angular) must handle all HTTP exceptions anyway. We don’t have a lot of custom exceptions, but one example. If a user has not accessed we can return HTTP 403 Forbidden. And a client can process it in a standard way(show an alert with a standard message). But if a client has not a license we can return 403 with the body(errorCode, message). The client has the next logic: if I get 403 I must check the body. If the body is empty I will show the standard message, else I will check the error code and process it. The client works with standard HTTP codes and shows custom errors in custom ways.
    2. Sometimes developers create one big enum with all error codes. It is magnetic dependencies(all classes have a dependency on this enum).
    3. What about clients? If client receives error message like `String userMessage = messageSource.getMessage(ErrorCode.BAD_CONFIG.name(), null, request.getLocale());` it will looks like `your config is bad`. How should client process this exception? parse it? I mean if it must show a custom message for a specific error

  3. While a bit reluctant initially, I already went along with these recommendations several times in production. I must admit that I find the techniques described here very ergonomic, from a development point of view, and very adaptable to many (if not most) UI error presentation techniques (web-based or otherwise).

    Just one custom “application exception” and/or a generic exception handler can easily cater to the most problematic situations. Pair this with the Problem Details for HTTP APIs RFC (https://datatracker.ietf.org/doc/html/rfc7807) and there you go, one less cross-cutting concern to worry about when designing your app…

    Thanks!

    1. Thanks for your feedback. The generic error reporting mechanism fits also very well with automatic validation of Dtos using @Valid and javax.validation annotations, if you convert the ConstraintViolatioNException into such syntax on the response json.

Leave a Reply to victorrentea Cancel reply

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