Spring Boot REST API Error Handling Guide

Editor’s note: This article was updated on September 5, 2022, by our editorial team. It has been modified to include recent sources and to align with our current editorial standards.

Handling errors effectively in APIs while delivering informative error messages is crucial. This helps the API client address issues gracefully. However, the standard behavior often returns stack traces that are difficult to interpret and unhelpful for the API client. To enhance clarity, structuring error information into distinct fields allows the API client to parse it and provide more user-friendly error messages. This article delves into how to implement robust Spring Boot exception handling when developing a REST API.

Person confused about a cryptic and long error message

Building REST APIs with Spring has become the de facto approach for Java developers, and Spring Boot simplifies this process significantly. It eliminates a substantial amount of boilerplate code and enables auto-configuration for numerous components. This article assumes a basic understanding of API development using these technologies. If you’re new to building REST APIs, it’s recommended to start with resources on Spring MVC or guides on creating a Spring REST Service.

Enhancing Error Response Clarity

We’ll use the source code hosted on GitHub as a practical example to demonstrate a REST API for retrieving bird objects. This application incorporates the features discussed in this article and includes additional error handling scenarios. Here’s a concise overview of the endpoints implemented:

GET /birds/{birdId}Gets information about a bird and throws an exception if not found.
GET /birds/noexception/{birdId}This call also gets information about a bird, except it doesn't throw an exception when a bird doesn't exist with that ID.
POST /birdsCreates a bird.

Spring framework’s MVC module offers excellent error handling capabilities. However, it’s the responsibility of the developer to leverage these features effectively, process exceptions, and return meaningful responses to the API client.

Consider an example where we send an HTTP POST request to the /birds endpoint with a JSON object containing the string “aaa” in the “mass” field, which expects an integer:

1
2
3
4
5
6
{
 "scientificName": "Common blackbird",
 "specie": "Turdus merula",
 "mass": "aaa",
 "length": 4
}

The typical Spring Boot response, without customized error handling, would look like this:

1
2
3
4
5
6
7
8
{
 "timestamp": 1658551020,
 "status": 400,
 "error": "Bad Request",
 "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
 "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]",
 "path": "/birds"
}

While the Spring Boot DefaultErrorAttributes-generated response contains relevant fields, it primarily focuses on the technical aspects of the exception. The timestamp field is represented as an integer without indicating its unit of measurement. The exception field is only useful for Java developers, and the message itself might leave the API consumer overwhelmed with implementation details that hold no relevance to them. Let’s explore how to extract more meaningful information from exceptions and package them into a more structured JSON representation to improve the experience for our API clients.

Before proceeding, since we’ll be working with Java date and time classes, we need to add a Maven dependency for the Jackson JSR310 converters. These converters facilitate the transformation of Java date and time objects into JSON representation using the @JsonFormat annotation:

1
2
3
4
<dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Let’s define a class named ApiError to represent API errors effectively. This class will encapsulate relevant error information encountered during REST calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ApiError {

   private HttpStatus status;
   @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
   private LocalDateTime timestamp;
   private String message;
   private String debugMessage;
   private List<ApiSubError> subErrors;

   private ApiError() {
       timestamp = LocalDateTime.now();
   }

   ApiError(HttpStatus status) {
       this();
       this.status = status;
   }

   ApiError(HttpStatus status, Throwable ex) {
       this();
       this.status = status;
       this.message = "Unexpected error";
       this.debugMessage = ex.getLocalizedMessage();
   }

   ApiError(HttpStatus status, String message, Throwable ex) {
       this();
       this.status = status;
       this.message = message;
       this.debugMessage = ex.getLocalizedMessage();
   }
}
  • The status property indicates the operation’s status code. Values in the 4xx range signify client errors, while 5xx codes signal server errors. For instance, an HTTP 400 (BAD_REQUEST) code might occur if the client submits an invalid email address.

  • The timestamp property records the date and time when the error took place.

  • The message property provides a user-friendly explanation of the error.

  • The debugMessage property contains a detailed system message describing the error.

  • The subErrors property holds an array of sub-errors, particularly relevant when multiple errors arise from a single call, such as multiple validation failures. The ApiSubError class encapsulates information about these sub-errors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
abstract class ApiSubError {

}

@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class ApiValidationError extends ApiSubError {
   private String object;
   private String field;
   private Object rejectedValue;
   private String message;

   ApiValidationError(String object, String message) {
       this.object = object;
       this.message = message;
   }
}

The ApiValidationError class extends ApiSubError and represents validation issues identified during the REST call.

Below are examples of JSON responses generated after implementing these refinements.

Here’s a JSON example returned when a requested entity is not found, such as when calling the endpoint GET /birds/2:

1
2
3
4
5
6
7
{
 "apierror": {
   "status": "NOT_FOUND",
   "timestamp": "22-07-2022 06:20:19",
   "message": "Bird was not found for parameters {id=2}"
 }
}

Here’s another JSON example returned when sending a POST /birds request with an invalid “mass” value for the bird:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
 "apierror": {
   "status": "BAD_REQUEST",
   "timestamp": "22-07-2022 06:49:25",
   "message": "Validation errors",
   "subErrors": [
     {
       "object": "bird",
       "field": "mass",
       "rejectedValue": 999999,
       "message": "must be less or equal to 104000"
     }
   ]
 }
}

Spring Boot Error Handling Mechanism

Let’s examine some Spring annotations crucial for handling exceptions.

RestController is the fundamental annotation for classes designed to handle REST operations.

ExceptionHandler is a Spring annotation that offers a way to manage exceptions thrown during the execution of handlers (controller operations). When applied to methods within controller classes, this annotation acts as the entry point for handling exceptions specifically within that controller.

Commonly, @ExceptionHandler is used in conjunction with @ControllerAdvice classes. This enables global or controller-specific exception handling within Spring Boot.

ControllerAdvice acts as “advice” applicable to multiple controllers, allowing a single ExceptionHandler to be applied across them. This centralized approach handles thrown exceptions for all classes governed by this ControllerAdvice.

The scope of affected controllers can be narrowed down using selectors within @ControllerAdvice, such as annotations(), basePackageClasses(), and basePackages(). If no selectors are specified, the ControllerAdvice is applied globally.

By combining @ExceptionHandler and @ControllerAdvice, we establish a central point for processing exceptions and encapsulating them within our ApiError object, resulting in a more organized approach compared to Spring Boot’s default error-handling mechanism.

Handling Exceptions

Representation of what happens with a successful and failed REST client call

Next, we’ll create a class named RestExceptionHandler to handle exceptions. This class must extend Spring Boot’s ResponseEntityExceptionHandler. By extending this class, we inherit some basic handling for Spring MVC exceptions. We will then introduce handlers for new exceptions while refining the handling of existing ones.

Overriding Exception Handling in ResponseEntityExceptionHandler

Examining the source code of ResponseEntityExceptionHandler reveals several methods named handle******(), such as handleHttpMessageNotReadable() or handleHttpMessageNotWritable(). Let’s illustrate how to override handleHttpMessageNotReadable() to manage HttpMessageNotReadableException exceptions. We can achieve this by overriding the method within our RestExceptionHandler class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

   @Override
   protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
       String error = "Malformed JSON request";
       return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex));
   }

   private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
       return new ResponseEntity<>(apiError, apiError.getStatus());
   }

   //other exception handlers below

}

With this override, a thrown HttpMessageNotReadableException will return an ApiError object with the message “Malformed JSON request.” Here’s how the response to a REST call would appear:

1
2
3
4
5
6
7
8
{
 "apierror": {
   "status": "BAD_REQUEST",
   "timestamp": "22-07-2022 03:53:39",
   "message": "Malformed JSON request",
   "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]"
 }
}

Implementing Custom Exceptions

Now, let’s define a method to handle an exception not inherently managed by Spring Boot’s ResponseEntityExceptionHandler.

A common scenario in Spring applications interacting with databases involves providing a method to retrieve a record by its ID using a repository. However, the CrudRepository.findOne() method returns null if the requested object is not found. If our service layer directly returns this result to the controller, we would receive an HTTP 200 (OK) code even if the resource is non-existent. The correct approach, as per the HTTP/1.1 spec, is to return an HTTP 404 (NOT FOUND) code.

To address this, we’ll introduce a custom exception called EntityNotFoundException. This differs from javax.persistence.EntityNotFoundException by providing constructors for easier object creation, and you might choose to handle the javax.persistence exception differently.

Example of a failed REST call

Let’s create an ExceptionHandler for this EntityNotFoundException within our RestExceptionHandler class. We’ll define a method named handleEntityNotFound() annotated with @ExceptionHandler, passing EntityNotFoundException.class. This instructs Spring to invoke this method whenever an EntityNotFoundException is thrown.

When using the @ExceptionHandler annotation, you can specify various auto-injected parameters like WebRequest, Locale, and others, as detailed here. In this handleEntityNotFound method, we’ll provide the EntityNotFoundException as a parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
  
   //other exception handlers
  
   @ExceptionHandler(EntityNotFoundException.class)
   protected ResponseEntity<Object> handleEntityNotFound(
           EntityNotFoundException ex) {
       ApiError apiError = new ApiError(NOT_FOUND);
       apiError.setMessage(ex.getMessage());
       return buildResponseEntity(apiError);
   }
}

Within the handleEntityNotFound() method, we set the HTTP status code to NOT_FOUND and utilize the new exception message. Here’s how the response for the GET /birds/2 endpoint would look:

1
2
3
4
5
6
7
{
 "apierror": {
   "status": "NOT_FOUND",
   "timestamp": "22-07-2022 04:02:22",
   "message": "Bird was not found for parameters {id=2}"
 }
}

The Significance of Spring Boot Exception Handling

Controlling exception handling is paramount for accurately mapping exceptions to our ApiError object and providing meaningful information to API clients. You would typically create additional handler methods (annotated with @ExceptionHandler) for other exceptions thrown within your application code. For more comprehensive examples covering common exceptions such as MethodArgumentTypeMismatchException and ConstraintViolationException, refer to the GitHub code.

Here are supplementary resources that contributed to this article:

Licensed under CC BY-NC-SA 4.0