Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

REST API Error Handling With Spring Boot

DZone 's Guide to

REST API Error Handling With Spring Boot

Want to learn more about room for improvement in error handling? Check out this tutorial on REST API error handling in Spring Boot.

· Java Zone ·
Free Resource

Introduction

Spring Boot provides a great exception handling mechanism out of the box. The default implementation ofErrorController does a good job of catching and handling exceptions. Also, we still can define our own @ExceptionHandler s to catch and handle specific exceptions. But, there is still room for improvement:

  • Even if we decide to handle all exceptions using a custom @ExceptionHandler, some exceptions will still manage to escape that handler and theErrorController would be responsible for exception handling. This@ExceptionHandler vs. ErrorController duality can definitely be improved.
  • Sometimes, the default error representation looks a bit messy:
{
  "timestamp": "2018-09-23T15:05:32.681+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "NotBlank.dto.name",
        "NotBlank.name",
        "NotBlank.java.lang.String",
        "NotBlank"
      ],
      "arguments": [
        {
          "codes": [
            "dto.name",
            "name"
          ],
          "arguments": null,
          "defaultMessage": "name",
          "code": "name"
        }
      ],
      "defaultMessage": "{name.not_blank}",
      "objectName": "dto",
      "field": "name",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotBlank"
    }
  ],
  "message": "Validation failed for object='dto'. Error count: 1",
  "path": "/"
}


Sure, we can improve this by registering a custom ErrorAttributes implementation, but a more sensible and opinionated (and still easily customizable) default representation would be appreciated.

  • For validation errors, one can easily expose some arguments from the constraint to the interpolated message. For example, the minimum amount for an int value can be passed to the interpolated message using the {} placeholder syntax. But, the same is not true for other exceptions:
public class UserAlreadyExistsException extends RuntimeException {

    // How can I expose this value to the interpolated message?
    private final String username;

    // constructors and getters and setters
}


  • It would be nice if we had built-in support for Application Level Error Codes for all exceptions. Sometimes, just by using an appropriate HTTP status code, we can't find out what exactly went wrong. For example, what if two totally different errors on the same endpoint have the same status code?

Room for Improvement

The errors-spring-boot-starter is an effort to provide a Bootiful, consistent, and opinionated approach to handle all sorts of exceptions. Built on top of Spring Boot's exception handling mechanism, the errors-spring-boot-starter offers:

  • A consistent approach to handle all exceptions — it doesn't matter if it's a validation/binding error or a custom domain-specific error or even a Spring related error. All of them would be handled by a WebErrorHandler implementation (no more ErrorController vs @ExceptionHandler)
  • Built-in support for application specific error codes, again, for all possible errors.
  • Simple error message interpolation using MessageSource.
  • Customizable HTTP error representation.
  • Exposing arguments from exceptions to error messages.

Default Error Representation

By default, errors would be a JSON result with the following schema:

// For each error, there is a code/messsge combination in the errors array
{
  "errors": [
    {
      "code": "first_error_code",
      "message": "1st error message"
    }
  ]
}


In order to customize this representation, just register the HttpErrorAttributesAdapter implementation as a Spring Bean.

Consistent Error Handling Approach

All exceptions would be handled by a WebErrorHandlerimplementation. By default, this starter would consult a few built-in WebErrorHandler methods to handle the following particular exceptions:

  • An implementation to handle all validation/binding exceptions.
  • An implementation to handle custom exceptions annotated with the @ExceptionMapping.
  • An implementation to handle Spring MVC specific exceptions.
  • And if the Spring Security is on the classpath, an implementation to handle Spring Security specific exceptions.

You can easily register your own exception handler by implementing anWerErrorHandler implementation and registering it as a Spring Bean.

Built-in Error Code Support

Although using appropriate HTTP status codes is a recommended approach in RESTful APIs, sometimes, we need more information to find out what exactly went wrong. This is where Error Codes comes in. You can think of an error code as a Machine Readable description of the error. Each exception can be mapped to at least one error code.

The exception-to-error-code mappings vary based on the exception type:

  • Validation error codes will be extracted from the message attribute of the corresponding constraint annotation, e.g. @NotBlank(message = "name.required").
  • The errorCode attribute for exceptions annotated with the @ExceptionMapping looks like the following:
@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {}


  • Here is the code from the custom implementations of WebErrorHandler :
public class ExistedUserHandler implements WebErrorHandler {

    @Override
    public boolean canHandle(Throwable exception) {
        return exception instanceof UserAlreadyExistsException;
    }

    @Override
    public HandledException handle(Throwable exception) {   
        return new HandledException("user.already_exists", BAD_REQUEST, null);
    }
}


Exposing Arguments

As with Spring Boot, you can pass validation arguments from the constraint annotation, e.g. @Min(value = 18, message = "age.min"), to the to-be-interpolated message:

age.min = The minimum age is {0}!


In addition, to support this feature for validation errors, we extend it for custom exceptions using the @ExposeAsArg annotation. For example, if we're going to specify the already taken username in the following message:

user.already_exists=Another user with the '{0}' username already exists


You can write:

@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {
    @ExposeAsArg(0) private final String username;

    // constructor
}


Conclusion

In this article, we enumerated a few possible improvements over Spring Boot's exception handling approach by introducing the errors-spring-boot-starter starter package. For more information about this starter, check out this GitHub repository.

Topics:
java ,spring ,spring boot ,exception handling ,tutorial ,errors ,string

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}