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.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
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 WebErrorHandler
implementation. 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 anWebErrorHandler
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.
Opinions expressed by DZone contributors are their own.
Comments