Skip to main content

Spring Boot Exception Handling

Spring Process Exception

Handling errors correctly in APIs while providing meaningful error messages is a very desirable feature, as it can help the API client properly respond to issues.

Once we see exception in Spring Boot, the default configuration will throw the below format of response.

Let’s say we have a controller named ProductController whose getProduct(...) method is throwing a NoSuchElementFoundException runtime exception when a Product with a given id is not found:

@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
//constructor omitted for brevity...

@GetMapping("/{id}")
public Response getProduct(@PathVariable String id){
// this method throws a "NoSuchElementFoundException" exception
return productService.getProduct(id);
}

}

Default Spring Boot Exception Response:
======================================
{
"timestamp": "2020-11-28T13:24:02.239+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/product/1"
}

The exception response payload is not giving us any useful information. Even the message field is empty, which we might want to contain something like “Item with id 1 not found”.

Spring Boot provides some properties which we can add to get error response properly. Using these Spring Boot server properties in our application.yml we can alter the error response to some extent.

server:
error:
include-message: always
include-binding-errors: always
include-stacktrace: on_trace_param
include-exception: false

After the above configuration is added then we can response like below.

{
"timestamp": "2020-11-29T09:42:12.287+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Item with id 1 not found",
"trace": "io.reflectoring.exception.exception.NoSuchElementFoundException: Item with id 1 not found...",
"path": "/product/1"
}

@ResponseStatus​

As the name suggests, @ResponseStatus allows us to modify the HTTP status of our response. It can be applied in the following places:

On the exception class itself

Along with the @ExceptionHandler annotation on methods

Along with the @ControllerAdvice annotation on classes

NOTE: This will give proper status code otherwise for all errors we will see only 500 status code

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}

Another way to achieve the same is by extending the ResponseStatusException class:

public class NoSuchElementFoundException extends ResponseStatusException {

public NoSuchElementFoundException(String message){
super(HttpStatus.NOT_FOUND, message);
}

@Override
public HttpHeaders getResponseHeaders() {
// return response headers
}
}
info

This approach comes in handy when we want to manipulate the response headers, too, because we can override the getResponseHeaders() method.

@ExceptionHandler​

The @ExceptionHandler annotation gives us a lot of flexibility in terms of handling exceptions. For starters, to use it, we simply need to create a method either in the controller itself or in a @ControllerAdvice class and annotate it with @ExceptionHandler:

@RestController
@RequestMapping("/product")
public class ProductController {

private final ProductService productService;

//constructor omitted for brevity...

@GetMapping("/{id}")
public Response getProduct(@PathVariable String id) {
return productService.getProduct(id);
}

@ExceptionHandler(NoSuchElementFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<String> handleNoSuchElementFoundException(
NoSuchElementFoundException exception
) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(exception.getMessage());
}

}

The exception handler method takes in an exception or a list of exceptions as an argument that we want to handle in the defined method. We annotate the method with @ExceptionHandler and @ResponseStatus to define the exception we want to handle and the status code we want to return.

If we don’t wish to use these annotations, then simply defining the exception as a parameter of the method will also do:

@ExceptionHandler
public ResponseEntity<String> handleNoSuchElementFoundException(
NoSuchElementFoundException exception)

@ControllerAdvice​

Why is it called "Controller Advice"?

The term 'Advice' comes from Aspect-Oriented Programming (AOP) which allows us to inject cross-cutting code (called "advice") around existing methods. A controller advice allows us to intercept and modify the return values of controller methods, in our case to handle exceptions.

Controller advice classes allow us to apply exception handlers to more than one or all controllers in our application:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

public static final String TRACE = "trace";

@Value("${reflectoring.trace:false}")
private boolean printStackTrace;

@Override
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request
) {
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}

@ExceptionHandler(ItemNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<Object> handleItemNotFoundException(
ItemNotFoundException itemNotFoundException,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}

@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<Object> handleAllUncaughtException(
RuntimeException exception,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}

//....

@Override
public ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {

return buildErrorResponse(ex,status,request);
}

}

A couple of things are new which we will talk about in a while. One major difference here is that these handlers will handle exceptions thrown by all the controllers in the application and not just ProductController.

If we want to selectively apply or limit the scope of the controller advice to a particular controller, or a package, we can use the properties provided by the annotation:

@ControllerAdvice("com.reflectoring.controller"): we can pass a package name or list of package names in the annotation’s value or basePackages parameter. With this, the controller advice will only handle exceptions of this package’s controllers.

@ControllerAdvice(annotations = Advised.class): only controllers marked with the @Advised annotation will be handled by the controller advice.