How To Handle Exceptions in Spring Boot Application

In a previous post, boilerplate mapping code was replaced with MapStruct. In this article, exception handling will be introduced in our Spring Boot Application. There are multiple options of how to do it. However, the best approach is to create a single component that will be responsible for all exception handling logic. The other parts of the system will be unaware of the exception handling.

exception handling spring boot

Existing Problem

You could notice that exception handling is not covered in an initial How to Create Spring Boot Application code. At the moment, there are two main exceptions that can appear while performing with the API.

Entity Not Found

In a situation when we try to get/update/delete and entity by id which does not exist in a database an error will occur.

curl -i --location --request GET 'localhost:8080/items/1000' \
--header 'Content-Type: application/json' \
--data-raw ''

Response:

HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 21 May 2021 12:19:05 GMT
Connection: close

{
    "timestamp":"2021-05-21T12:19:05.553+00:00",
    "status":500,
    "error":"Internal Server Error",
    "message":"",
    "path":"/items/1000"
}

There are two problems here:

  • Response status is 500 but entity was not found in the system. Consequently, status 404 Not Found is expected.
  • The message does not have any information related to the problem.

Hibernate Validation Errors

Another place, where an exception can occur is during Hibernate Validation process.

Let’s try to create a product item with an empty title. It is forbidden to do because title is specified to have from 1 to 255 characters.

curl -i --location --request POST 'localhost:8080/items' \
--header 'Content-Type: application/json' \
--data-raw '{
    "title" : ""
}'

Response:

HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 21 May 2021 12:04:41 GMT
Connection: close

{
    "timestamp":"2021-05-21T12:04:41.354+00:00",
    "status":500,
    "error":"Internal Server Error",
    "message":"",
    "path":"/items"
}

As you can see there are the same two problems here:

  • Response status is 500. The title in the request has an incorrect format. Consequently, the status 400 Bad Request should be presented.
  • The message does not have any information related to the problem.
not found

Possible Approaches of Exception Handling

There are multiple ways of exception handling. E.g.

  • Catch an exception in a controller and return ResponseEntity with a set status code and body.
  • Create an exception with @ResponseStatus annotation. It can be thrown from any place.
  • Throw ResponseStatusException with status and reason from any place.
  • Use ErrorController for global exception handling.
  • Use @ExceptionHandler for exception handling in a single controller.
  • Use global @ControllerAdvice with @ExceptionHandler for global exception handling.

In our application @ControllerAdvice with @ExceptionHandler will be used because of the next reasons:

  • it allows to create a single global component for exception handling.
  • Response bodies can have different forms based on the specific needs.

Adding Controller Advice to Spring Boot Application

The whole code is presented in the following snipped.

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {

    @ResponseStatus(NOT_FOUND)
    @ExceptionHandler(EntityNotFoundException.class)
    public void handleNotFound(EntityNotFoundException e) {
        log.debug("Entity not found.", e);
    }

    @ResponseStatus(BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public ErrorsDto handleConstraintViolations(ConstraintViolationException e) {
        final List<ErrorDto> errors = e.getConstraintViolations()
                .stream()
                .map(v -> ErrorDto.builder()
                        .message(v.getMessage())
                        .path(v.getPropertyPath().toString())
                        .value(v.getInvalidValue())
                        .build())
                .collect(toList());

        return new ErrorsDto(errors);
    }

}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorsDto {

    private List<ErrorDto> errors;

}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorDto {

    private String message;

    private String path;

    private Object value;

}

To begin with, a class with annotation @RestControllerAdvice is created. It is our global handler. @RestControllerAdvice is a combination of @ControllerAdvice and @ResponseBody. By doing this @ResponseBody should not be specified on methods that are expected to respond with the body.

The next step is to add multiple @ExceptionHandler methods. The single method can be responsible for handling multiple exceptions.

For our cases we should have two handlers.

Entity Not Found

handleNotFound method is used for handling EntityNotFoundException.

    @ResponseStatus(NOT_FOUND)
    @ExceptionHandler(EntityNotFoundException.class)
    public void handleNotFound(EntityNotFoundException e) {
        log.debug("Entity not found.", e);
    }

In this case we only respond with 404 Not Found status code when EntityNotFoundException occurs. Response body is omitted.

Hibernate Validation

For this case handleConstraintViolations is used.

    @ResponseStatus(BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public ErrorsDto handleConstraintViolations(ConstraintViolationException e) {
        final List<ErrorDto> errors = e.getConstraintViolations()
                .stream()
                .map(v -> ErrorDto.builder()
                        .message(v.getMessage())
                        .path(v.getPropertyPath().toString())
                        .value(v.getInvalidValue())
                        .build())
                .collect(toList());

        return new ErrorsDto(errors);
    }

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorsDto {

    private List<ErrorDto> errors;

}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorDto {

    private String message;

    private String path;

    private Object value;

}

ConstraintViolationException occurs during Hibernate Validation.

It is the case of 400 Bad Request. As a result, @ResponseStatus(BAD_REQUEST) is used.

We can also have a more specific message about the problem. For this reason, ErrorsDto class is created. Information about an error message, a path, and a requested value will be returned to the user. The data are fetched from the ConstraintViolationException.

Returned body will have the next form e.g. when the user tries to create a product item with an empty title:

{
    "errors": [
        {
            "message": "size must be between 1 and 255",
            "path": "create.createItemRequest.title",
            "value": ""
        }
    ]
}

no way

Tests

Next tests were added to cover some of these cases (get item and create item).

    @Test
    public void shouldNotGetItem_NotFound() {
        final HttpEntity<Void> entity = new HttpEntity<>(httpHeaders());

        ResponseEntity<Item> foundItem = restTemplate.exchange(url("/items/-1"), HttpMethod.GET, entity, Item.class);
        assertThat(foundItem.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }    

    @Test
    public void shouldNotCreateItem_LongTitle() {
        final String title = Stream.generate(() -> "a")
                .limit(257)
                .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
                .toString();

        final HttpEntity<CreateItemDto> entity = new HttpEntity<>(
                new CreateItemDto(title),
                httpHeaders()
        );

        ResponseEntity<ErrorsDto> error = restTemplate.exchange(
                url("/items"),
                HttpMethod.POST,
                entity,
                ErrorsDto.class
        );

        assertThat(error.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(error.getBody()).isNotNull();
        assertThat(error.getBody().getErrors()).isNotNull();
        assertThat(error.getBody().getErrors()).hasSize(1);
        assertThat(error.getBody().getErrors().get(0).getMessage()).isEqualTo("size must be between 1 and 255");
        assertThat(error.getBody().getErrors().get(0).getPath()).isEqualTo("create.createItemRequest.title");
        assertThat(error.getBody().getErrors().get(0).getValue()).isEqualTo(title);
    }

Summary

To sum up, in this post global exception handling in Spring Boot Application was introduced.

The whole code can be found at Github v4.0.0-exception-handling branch.

The whole course:

Oleksii

Java development

You may also like...

Leave a Reply

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