Spring Boot Rest Error Handling with Controller Advice

Here we are going to understand how to manage errors in Spring Boot Rest applications.

To manage errors, Spring provides a special component name like controller advice.

This special component allows us to handle exceptions coming through controllers and form the global application and, looks like this:

You have to annotate your error handler class with @ControllerAdvice

@ControllerAdvice
public class ApiExceptionHandler {

		// manage errors

}

How to handle errors with a Controller Advice Component

Into our component @ControllerAdvice, we have to create a method with this annotation @ExceptionHandler to indicate which exception this method should manage.

Here, in the following example, we are catching all error types Exception and returning a ResponseEntity with legible information to the client.

A ResponseEntity is an extension class of HttpEntity and represents an HTTP response allowing to include status code, headers, and body.

@ControllerAdvice
public class ApiExceptionHandler {

    // Generic exception handler
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllExceptions(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred - " + ex.getMessage());
    }

}

In this controller, we have a Rest API with an intentional error for understanding purposes:


@RestController
public class DemoController {
    
    @GetMapping("/demo-error")
    public String demo() {
        //Calling service that produces an error
        throw new RuntimeException("Generic Runtime error");
    }

}

The result of this error will be like that.

Notice, the body message and the status code.

Spring Boot Controller Advice

Handle specific exceptions

To manage specific errors you can use @ExceptionHandler also, but you have to indicate your custom error instead of a generic error.

@RestController
public class DemoController {

    @GetMapping("/demo-error")
    public String demo() {
        //Calling service that produces an error
        throw new RuntimeException("Generic Runtime error");
    }

    @GetMapping("/demo-custom-error")
    public String demoCustomError() {
        //Calling service that produces a custom error
        throw new CustomException("Custom service error");
    }

}

This is the custom expectation that we created.

public class CustomException extends RuntimeException {

    public CustomException(String message) {
        super(message);
    }
}

Running /demo-custom-error

Spring Boot Controller Advice

Notice we can combine errors in the same method to handle similar errors.

@ExceptionHandler(value = {CustomException2.class, CustomException3.class})
public ResponseEntity<String> handleSeveralException(Exception ex) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("A custom error occurred - " + ex.getMessage());
}

Of course, the error status, messages and other values that we return must be consistent with the error

Exception Handler with Response Status annotation

In the previous examples we sent the status code into ResponseEntity but, you can use the @ResponseStatus annotation.

@ResponseStatus(HttpStatus.CONFLICT)  // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public HttpEntity<String> handleSeveralExceptionWithResponseStatus(DataIntegrityViolationException ex) {
    return new HttpEntity<>("A error with ResponseStatus annotation");
}

A new url controller to review this example:

@GetMapping("/demo-custom-error-status")
public String demoCustomErrorStatus() {
    //Calling exception that produces a custom error with ResponseStatus annotation
    throw new DataIntegrityViolationException("DataIntegrityViolationException exception error with ResponseStatus annotation");
}

spring boot controller advice

Extending from Response Entity Exception Handler

To address the most frequent errors that Spring raises, you can extend your advice component from the ResponseEntityExceptionHandler.class.

This ResponseEntityExceptionHandler class handles all the internal Spring exceptions and it is recommended to use it to return formatted errors.

Check the source code for this class and notice that it handles all of Spring’s internal exception classes

This is our control advice extending from ResponseEntityExceptionHandler

@ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    // Generic exception handler
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllExceptions(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred - " + ex.getMessage());
    }

    // Custom exception handler
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<String> handleCustomException(CustomException ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("A custom error occurred - " + ex.getMessage());
    }

    @ExceptionHandler(value = {CustomException2.class, CustomException3.class})
    public ResponseEntity<String> handleSeveralException(CustomException ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("A custom error occurred - " + ex.getMessage());
    }

}

Testing these examples

This test checks all of our examples.

@SpringBootTest
@AutoConfigureMockMvc
class DemoControllerTest {

    private final MockMvc mockMvc;

    DemoControllerTest(@Autowired MockMvc mockMvc) {
        this.mockMvc = mockMvc;
    }

    @Test
    void demo() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo-error"))
                .andDo(print())
                .andExpect(status().is5xxServerError())
                .andExpect(content().string(containsString("An error occurred - Generic Runtime error")));
    }

    @Test
    void demoCustomError() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo-custom-error"))
                .andDo(print())
                .andExpect(status().is5xxServerError())
                .andExpect(content().string(containsString("A custom error occurred - Custom exception error")));
    }

    @Test
    void demoCustomErrorStatus() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo-custom-error-status"))
                .andDo(print())
                .andExpect(status().isConflict()) // status 409
                .andExpect(content().string(containsString("A error with ResponseStatus annotation")));
    }
}

If you don’t know how to test @RestController, see this post about it.

Conclusion

You can use @ControllerAdvice and @ExceptionHandler to handle exceptions in your Spring Boot applications to send a proper response to the client with enough information about the reason for the error.

It is convenient to extend your control advice from ResponseEntityExceptionHandler to handle the Spring exception.

Remember, the message and especially the HTTP status should be valid and must agree with the code convention. Check this good guide about HTTP codes

Hi! If you find my posts helpful, please support me by inviting me for a coffee :)

See also