DEV Community

Sergey Chin
Sergey Chin

Posted on

Custom exception handling in Spring Boot

Introduction

One of the important tasks in creating an API is to return understandable error messages. Spring Boot already has a predefined error message format, but this format is not always acceptable and our application may require a custom format.

In this tutorial, we'll configure Spring Boot's exception handling so that our backend application will respond with an error message in the following format:

{
    "guid": "DCF70619-01D8-42a9-97DC-6005F205361A",
    "errorCode": "application-specific-error-code",
    "message": "Error message",
    "statusCode": 400,
    "statusName": "BAD_REQUEST",
    "path": "/some/path",
    "method": "POST",
    "timestamp": "2022-12-06"
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the format:

  • guid — unique global identifier of the error, this field is useful for searching errors in a large log.
  • errorCode — an application specific error code derived from business logic rules.
  • message — description of an error.
  • statusCode — HTTP status code.
  • statusName — full name of the HTTP status code.
  • path — URI of the resource where the error occurred.
  • method — used HTTP method.
  • timestamp — timestamp of the error creation.

Implementation

As an example we will make REST API for working with city list.

After creating Spring Boot project via Spring Initializer or manually, add next classes two classes to the project: ApplicationException and ApiErrorResponse.

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public class ApplicationException extends RuntimeException {
    private final String errorCode;
    private final String message;
    private final HttpStatus httpStatus;
}
Enter fullscreen mode Exit fullscreen mode

ApplicationException will be thrown, when exception situation occurred in the application.

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class ApiErrorResponse {
    private final String guid;
    private final String errorCode;
    private final String message;
    private final Integer statusCode;
    private final String statusName;
    private final String path;
    private final String method;
    private final LocalDateTime timestamp;
}
Enter fullscreen mode Exit fullscreen mode

ApiErrorResponse is DTO object for serializing to JSON response.

Next, add class ApplicationExceptionHandler that will be handling all exceptions of the application.

import io.github.sergiusac.exceptionhandling.response.ApiErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.UUID;

@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity<?> handleApplicationException(
            final ApplicationException exception, final HttpServletRequest request
    ) {
        var guid = UUID.randomUUID().toString();
        log.error(
                    String.format("Error GUID=%s; error message: %s", guid, exception.getMessage()), 
            exception
        );
        var response = new ApiErrorResponse(
                guid,
                exception.getErrorCode(),
                exception.getMessage(),
                exception.getHttpStatus().value(),
                exception.getHttpStatus().name(),
                request.getRequestURI(),
                request.getMethod(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(response, exception.getHttpStatus());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleUnknownException(
        final Exception exception, final HttpServletRequest request
    ) {
        var guid = UUID.randomUUID().toString();
        log.error(
            String.format("Error GUID=%s; error message: %s", guid, exception.getMessage()), 
            exception
        );
        var response = new ApiErrorResponse(
                guid,
                ErrorCodes.INTERNAL_ERROR,
                "Internal server error",
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                HttpStatus.INTERNAL_SERVER_ERROR.name(),
                request.getRequestURI(),
                request.getMethod(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

}
Enter fullscreen mode Exit fullscreen mode

We use RestControllerAdvice annotation to create component that handles exceptions globally in the application, and ExceptionHandler annotation is used to specify the exceptions.

handleApplicationException method handles all exceptions of ApplicationException class. This method creates GUID for exception, writes the exception to logs and makes error response for client.

handleUnknownException method does similar actions, but for all other exceptions.

Next, we create CityService for working with city list.

import io.github.sergiusac.exceptionhandling.exception.ApplicationException;
import io.github.sergiusac.exceptionhandling.model.City;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.stream.Collectors;

@Service
public class CityService {

    private final Map<Long, City> cities = new ConcurrentHashMap<>() {
        {
            put(1L, new City(1L, "Paris", LocalDateTime.now(), LocalDateTime.now()));
            put(2L, new City(2L, "New-York", LocalDateTime.now(), LocalDateTime.now()));
            put(3L, new City(3L, "Barcelona", LocalDateTime.now(), LocalDateTime.now()));
        }
    };

    public List<City> getAllCities() {
        return cities.values().stream().collect(Collectors.toUnmodifiableList());
    }

    public City getCityById(final Long id) {
        var city = cities.get(id);
        if (city == null) {
            throw new ApplicationException(
                    "city-not-found",
                    String.format("City with id=%d not found", id),
                    HttpStatus.NOT_FOUND
            );
        }
        return city;
    }

}
Enter fullscreen mode Exit fullscreen mode

The service has built-in list of cities and two methods for viewing the city list. Also getCityById method throws special ApplicationException, with provided information like error code, message and HTTP status.

Next, we create REST controller.

import io.github.sergiusac.exceptionhandling.service.CityService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/cities")
public class CityController {

    private final CityService cityService;

    @GetMapping
    public ResponseEntity<?> getAllCities() {
        return ResponseEntity.ok(cityService.getAllCities());
    }

    @GetMapping("/{id}")
    public ResponseEntity<?> getCityById(@PathVariable final Long id) {
        return ResponseEntity.ok(cityService.getCityById(id));
    }

}
Enter fullscreen mode Exit fullscreen mode

This controller just exposes two methods similar to the CityService.

After compiling and running the application, we should see the next result:

GET http://localhost:8080/cities

[
  {
    "id": 1,
    "cityName": "Paris",
    "createdAt": "2022-12-06T22:06:18.6921738",
    "updatedAt": "2022-12-06T22:06:18.6921738"
  },
  {
    "id": 2,
    "cityName": "New-York",
    "createdAt": "2022-12-06T22:06:18.6921738",
    "updatedAt": "2022-12-06T22:06:18.6921738"
  },
  {
    "id": 3,
    "cityName": "Barcelona",
    "createdAt": "2022-12-06T22:06:18.6921738",
    "updatedAt": "2022-12-06T22:06:18.6921738"
  }
]
Enter fullscreen mode Exit fullscreen mode

As we can see, the first method returns valid response containing list of three cities.

But when we try to get city with unknown ID we get the next error response:

GET http://localhost:8080/cities/4

{
  "guid": "01913964-5777-4ec1-bd5e-392c5a5fecc9",
  "errorCode": "city-not-found",
  "message": "City with id=4 not found",
  "statusCode": 404,
  "statusName": "NOT_FOUND",
  "path": "/cities/4",
  "method": "GET",
  "timestamp": "2022-12-06T22:10:37.8993481"
}
Enter fullscreen mode Exit fullscreen mode

And in the application log we also can see the appropriate error message:

Screenshot with logs

Conclusion

Implementing custom exception handlers made simple with Spring Boot, using RestControllerAdvice and ExceptionHandler annotations we implemented global error handling consolidated in only one component.

The provided code in this article is only for demonstrating purposes, and you should consider different aspects of your application while implementing custom exception handling logic.

Thanks for reading, good coding🤖

Top comments (0)