All Articles

Error handling - Correct status and message approach

Errors are fine, so lang as you handle them gracefully.
Errors are fine, so lang as you handle them gracefully.

Error handling is quite an annoying subject, but it can be very helpful to the consumer. First of, you are already shielding your application from giving out such information as to the platform it is running on, the language used, etc. Also, everybody has already seen errors - it’s fine, they happen. However, it is so much better if the UI handles them gracefully, rather than seeing an ugly stacktrace. Most people are not very technical, so if they see something like an ArrayOutOfBoundsException, they simply do not have a clue what this could mean.

There are two things that are important in an erroneous response, the status and the message.

Error status

The error status is the most important part of the error response in my opinion. These are defined, universally understood HTTP status codes (btw, the plural of status is status). They will immediately let the consumer know what type of error has occurred, even if they do not know anything about your application.

A list of them can be found here, but I will briefly cover all those that are the most common and which I will also use. While 418 (I’m a teapot) may be funny, it’s not the most meaningful and should be avoided for example.

2xx success status codes

There are not too many success responses I typically use, but they all have small differences.

200 (OK)

This is simply what I’ll use when nothing special has happened. You made a request, and you get your response.

201 (Created)

I will use this response code when a new resource has been created, and should usually only occur in POST requests. Usually, this response is accompanied with the information containing the URI of the newly created resource.

204 (No content)

This is the type of response code that I am a big fan of, but that I don’t usually see much in projects. I will use this often in update functionality, as I find it a better status than simply giving OK. For a 200 OK, I will usually provide some response.

207 (Multi-status)

Now, this is a code I will use very sparingly. In essence, it is usually used when performing multiple operations in one go (like a multi-create or multi-update), and you provide one HTTP code per block of the response. So, if you would usually get one 201 and one 400, these codes will now be part of the response. I’m not the biggest fan of this code, as it often requires complex$ logic on the consumer side, and it’s also not the easiest to create, as you need to catch errors and then package them in response blocks.

4xx error status codes

The errors containing these codes are usually defined by the application.

400 (Bad Request)

A very simply status, usually meaning that the request is malformed.

401 (Unauthorized)

This is in itself a simple code - it means you’re unauthorized to perform that operation. If I’m honest though, I often confuse it with 403, as they are very similar. 401 should be preferred when the error is due to authentication not having been provided or being insufficient.

403 (Forbidden)

As opposed to 401, this means that authentication has been successful, yet the user is not allowed to perform the operation.

404 (Not Found)

Maybe the most famous error code of all, and requires no introduction. The resource has not been found.

409 (Conflict)

I like this code when the error behind it is some sort of database locking, and if multiple users are doing similar actions. It will most likely not come into play much in our application, though.

5xx server errors

These http error codes are not usually specified by the application, but rather the infrastructure around it. Whenever you see one of these error codes, that means that some additional error handling is most likely required.

Error message

The error message is used to give additional information to the consumer about what went wrong. Imagine getting a 404 on an endpoint like /persons/{personId}/addresses/{addressId}. You would not be able to tell whether the person or the address is not found, and this could now be specified in the error message.

Very well, now I will make one additional distinction of how the error message should look.

Internal APIs

In internal APIs, the error message I use are generally enums. The main reason is that I/my team are working on both the client and the server. So essentially, we ourselves are the consumers of the endpoints. It’s therefore very easy to handle the errors, as we know which errors can be expected in which places.

Additionally, if we have internationalization in the frontend application, it is easiest to handle the displaying of the error in the correct language in there. The server does not need to know which language the UI is displayed in, yet it will still be translated in the language the user expects.

As such, an error message would be UNKNOWN_ACCOUNT, and it would be displayed as No account could be found for the given ID in English, but a French user would see Aucun compte n’a pu être trouvé pour cette ID.

External APIs

For APIs that are used by external users, I would simply provide an error message in English, as that is the main coding language. Since they are most likely calling us from a different server, they can then handle the error more gracefully on their end.

Of course, if there has been a special agreement in the API, and it’s not defined only by yourself, a mixture of both methods above can be implemented.

Application fix

In the last article, we defined one error already, for when an account could not be found by an ID. However, I realized that simply having the @ResponseStatus did not seem to be sufficient for the error handling to work - the application was returning 401 instead of the defined 404. This will now be fixed.

The error occurs due to a feature in Spring Boot where, once an error occurs, an additional POST request is made to /error. We have not specified this endpoint to be exempted from the authentication, however, therefore spring boot returns the 401.

This means that the fix is very simple, as you should only add /error/** to your antmatcher exemptions, and then it works. It now looks as follows:

@Component
class AppWebSecurityConfigurerAdapter : WebSecurityConfigurerAdapter() {

    override fun configure(web: WebSecurity?) {
        web?.ignoring()?.antMatchers("/actuator/*", "/accounts", "/accounts/**", "/error/**")
    }
}

If we try the request again, we now get the response in the image below.

Correct status, but no message
The status now reflects the correct error code.

Hold on though. We did define a message also. Where is that then? Again, depending on your spring boot version, the message is by default not present in the response (it was removed in version 2.3.0).

To let spring know that you’d like to pass the message also, add the following property in the application.yaml:

server:
  error:
    include-message: always

Start your application once more (I’ve also changed the message to ACCOUNTNOTFOUND as opposed to the text we had earlier), and you’ll get the following response for that 404:

Correct status and message in error response
The status now reflects the correct error code.

Now, just to test that the error handling with @ResponseStatus works as expected, I have added the following endpoint for testing (and commented out the previous endpoint for that URL):

    @GetMapping("/accounts/{httpCode}")
    fun getAccounts(@PathVariable("httpCode") httpCode: Int) : ResponseEntity<Nothing> {
        return when(httpCode){
            201 -> ResponseEntity.status(HttpStatus.CREATED).body(null)
            403 -> throw ForbiddenException()
            418 ->  throw TeapotException()
            else -> throw BadRequestException()
        }
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    class BadRequestException() : RuntimeException(){}

    @ResponseStatus(HttpStatus.FORBIDDEN)
    class ForbiddenException() : RuntimeException(){}
   
    @ResponseStatus(HttpStatus.I_AM_A_TEAPOT)
    class TeapotException() : RuntimeException(){}

Now you get the following responses:

Different routes for different components
Created response
Yes, created, as you wished!
Bad request response
Arrrghhh, no idea what you want from me...
Forbidden response
No entry for you!
Teapot response
Want to be funny with a teapot?

There we go. Everything is now working peachy, and you can handle your errors much more elegantly.

Note for @ResponseStatus

As you may have noticed already, @ResponseStatus does have some limitations. You can not provide that many fields, for example. In cases where a more elaborate error handling will be required, I may want to use different solutions, such as @ControllerAdvice, HandlerExceptionResolver, @ExceptionHandler, or the new ResponseStatusException (since Spring 5+).

However, for now, @ResponseStatus is a very easy way to get started, and it does suffice for our need.

For now, I wish you all the fun and creativity you want in handling those pesky errors!