Error handling – for me this phrase associates with angry frontend developers accusing backend team of throwing exceptions without any unified response format into ether.

Let’s imagine someone has failed during the development or planning process, and now we need to fix the things – how to unify the error responses with Spring Boot? My favourite is usage of @ControllerAdvice. It is annotation used to define class that contains @ExceptionHandler annotated methods which define how to handle exceptions.

Custom error responses

To unify the logic, we need to prepare:

  1. Response body class – used in each error response.
  2. Common method to return the same format for each handled exception.
  3. Exception handler classes to define how to handle given exceptions.

Preparation

Assuming we are working on errors for employee manager, and we have employees/{id} endpoint which throws EmployeeNotFoundException when employee doesn’t exist, let’s create/add spock integration test in EmployeesControllerTest:

    def "should respond with NOT_FOUND Http status code when user with given firstName doesn't exist"() {
        when:
        def resultingStatusCode
        def resultingBody
        try {
            restTemplate.getForEntity("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
        } catch (HttpStatusCodeException e) {
            resultingStatusCode = e.statusCode
            resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
        }

        then:
        resultingStatusCode == HttpStatus.NOT_FOUND
        with(resultingBody) {
            message == "Employee (id = 'test-id') doesn't exist."
            code == "employee_not_found"
            details == [[employeeId: "test-id"]]
        }
    }

Of course test will fail as the response is completely different than what we expect:

response before spring

spring boot error handling

Response body

First of all we need to prepare body of exceptional response. Let’s name it ErrorResponse and put in the root of the project:

class ErrorResponse {
    private final String message;
    private final String code;          // code for frontend to tell which message it needs to show for user 
    private final List<Object> details; // ie. details which field in input failed
    
    // ... constructors, builders, getters, setters, equals, hashcode, toString ... but I prefer to use Lombok instead ;)
}

Don’t you think it would be nice to define enum with error codes, just in case if there will be other developers working with that code? Let’s create it:

enum ErrorCode {
   EMPLOYEE_NOT_FOUND("employee_not_found");

   private final String code;

   ErrorCode(String code) {
       this.code = code;
   }

   public String getCode() {
       return this.code;
   }
}

Common response

Then we need to create abstract class with a method that will define a response. Where? It depends on the project, but probably on the project’s root as we need to access it from each feature’s package.

public abstract class HttpResponseExceptionHandler {
    protected ResponseEntity<ErrorResponse> getErrorResponseEntity(
            Exception e,
            String errorCode,
            List<Object> details,
            HttpStatus status) {
        ErrorResponse errorResponse = new ErrorResponse(e.getMessage(), errorCode, details);
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.set("Content-type", MediaType.APPLICATION_JSON_VALUE);
        return new ResponseEntity<>(errorResponse, headers, status);
    }
}

Exception handler

Next thing is to prepare @ControllerAdvice annotated classes for each functionality package. It is our place to define exception handlers for functionality errors:

@ControllerAdvice
class EmployeeExceptionHandler extends HttpResponseExceptionHandler {
    
    @ExceptionHandler(value = {EmployeeNotFoundException.class})
    ResponseEntity<ErrorResponse> handleCustomerAlreadyExists(EmployeeNotFoundException e) {
        Map<String, String> detailsMap = Collections.singletonMap("employeeId", e.getEmployeeId());
        return getErrorResponseEntity(
                e,
                ErrorCode.EMPLOYEE_NOT_FOUND.getCode(),
                Collections.singletonList(detailsMap),
                HttpStatus.NOT_FOUND);
    }
}

Of course, in @ExceptionHandler’s value argument you can define more Exception classes if they fit the same mapping.

Verification

Run previously prepared test:

error handling spring boot

Now check the response:

handling error in spring boot

It works!

Overriding default Spring error responses

You’ve probably noticed that there are some default spring exception handlers. They were created to simplify responses and catch possible exceptions like HttpRequestMethodNotSupportedException. Let’s override mentioned one.

Traditionally, create test:

    def "should override default spring response for NotSupportedMethodException"() {
        when:
        def resultingStatusCode
        def resultingBody
        try {
            restTemplate.put("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
        } catch (HttpStatusCodeException e) {
            resultingStatusCode = e.statusCode
            resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
        }

        then:
        resultingStatusCode == HttpStatus.METHOD_NOT_ALLOWED
        with(resultingBody) {
            message == "Not supported HTTP method. Available methods are: [GET]"
            code == "not_supported_http_method"
        }
    }

http error handling in spring boot

Test is failing, so we need to override handleHttpRequestMethodNotSupported method from ResponseEntityExceptionHandler:

@ControllerAdvice
class SpringRESTExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
            HttpRequestMethodNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        pageNotFoundLogger.warn(ex.getMessage());

        Set<HttpMethod> supportedMethods = ex.getSupportedHttpMethods();
        if (!CollectionUtils.isEmpty(supportedMethods)) {
            headers.setAllow(supportedMethods);
        }
        ErrorResponse errorResponse = new ErrorResponse(
                String.format("Not supported HTTP method. Available methods are: %s", supportedMethods),
                "not_supported_http_method",
                null);
        return handleExceptionInternal(ex, errorResponse, headers, HttpStatus.METHOD_NOT_ALLOWED, request);
    }
}

Of course, you should replace "not_supported_http_method" with some enum or constant

Run the test:

common http error handling in spring boot

Let’s check the request now:

second after test spring

Conclusion

As you can see, it’s really straightforward solution. You can simply replace default Spring error responses as well as your custom exception error responses. Once the response format and @ControllerAdvice classes are defined, the only thing you have to do is to prepare new handler methods.

Would you like to talk with our experts about custom software development

Q: What is error handling in Spring Boot?

Error handling in Spring Boot refers to the process of defining how to handle exceptions that may occur in a Spring Boot application. This can be done using the @ControllerAdvice annotation to define a class that contains @ExceptionHandler annotated methods, which determine how to handle specific exceptions.

Q: What is the ErrorResponse class in Spring Boot error handling?

The ErrorResponse class is a class used to define the body of an exceptional response in Spring Boot error handling. It typically includes properties such as a message, code, and details about the error.

Q: What is the HttpResponseExceptionHandler class in Spring Boot error handling?

The HttpResponseExceptionHandler class is an abstract class that defines a method for creating a standardized response for exceptions in Spring Boot. This class can be extended by other exception handler classes to create a consistent format for error responses.

5/5 - (7 votes)