We have seen how spring boot helps us quickly put together RESTful web-services and web applications. When an application or service works as intended it leads to happy users and productivity. However, there are times when software may not be able to fulfill its functions – due to errors or exceptions. The causes of errors in software could be intrinsic such as bugs in its logic or extrinsic such as unavailability of memory or a network connection.
Importance of Exception Handling
Exception handling is an important feature of any application to maintain the normal flow of the application by handling unwanted or unexpected events that may arise during the execution of the application.
Exception handling in Spring is a very useful feature, because it helps the Spring REST application to properly respond to issues. The default behavior tends to return stack traces. These stack traces are difficult to understand and useless for the API client. In this article, we will discuss some common approaches to handle errors in a Spring Boot application. Spring boot provides excellent exception abstraction with the help of annotations. This feature frees programmers from writing huge blocks of code and thereby improves readability of their code.
Sample application
Let’s consider a simple library service where we can fetch books. Let’s define a simple model to define books as follows:
1 2 3 4 5 |
public class Book { private int isbn; private String name; private String author; } |
Let’s create a simple REST controller with a request mapping that expects the isbn id of a book and returns the Book with the given isbn number, if it is present:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@RestController public class LibraryController { private static List<Book> bookList = new ArrayList<>(); static { bookList.add(new Book(20041, "Getting things done", "David Allen")); bookList.add(new Book(30089, "Life of Pi", "Yann Martel")); bookList.add(new Book(67899, "The goal", "Eliyahu Goldratt")); } @GetMapping(value = "/book/{id}") public ResponseEntity<?> getBook(@PathVariable int id) { if (id < 0) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } Book book = findBook(id); if (book == null) { return ResponseEntity.status(HttpStatus.ENTITY_NOT_FOUND).build(); } return ResponseEntity.ok(book); } } |
Apart from just finding a book from the library, we also have to perform additional checks, like the book should have a valid isbn id that is always greater than 0, else we have to return a BAD_REQUEST status code.
Similarly, if the book is not found then we have to return a ENTITY_NOT_FOUND status code. Additionally, it is a good design to give a description about the error to the client by including a text.
For every check, we need to create a ResponseEntity object containing response codes along with a text containing the description of the error based on our requirements.
These checks have to be made multiple number of times, in case our APIs grows. For example, we are adding a new PATCH request mapping to update the books available in our library, we need to create these ResponseEntity objects again. This causes issues related to maintain consistency of the application.
It is mandatory to perform these checks in each RequestMapping. Instead of handling error scenarios by returning response code for each of them, we can throw an exception in case of a violation and handle them separately. We can either use built-in exceptions already provided Spring, or we can create our own exceptions, if needed. This will validate our error handling logic. We cannot return default server error messages to the client while serving a REST API. Neither, can we return stack traces that are complex to understand. Hence, proper exception handling with Spring is an essential feature of designing a REST API.
Common approaches of Exception handling provided by Spring
Having a proper exception handling approach is a essential for any application. The Spring MVC Framework provides the below smart approaches to provide exception handling. We will go through some of the commonly used Spring annotations used for exception handling.
- @ResponseStaus annotation – This is the straight forward way provided by Spring to respond the status of HTTP response by providing a status code. This annotation can be used on methods and classes.
- Controller Based – Spring provides many annotations to handle exceptions. In case of defining exception handler methods of controller classes, we can annotate these methods with @ExceptionHandler annotation. By doing this, Spring provides mechanism to handle exceptions that are thrown while executing controller classes. This will be used as an entry point for handling exceptions that are thrown within the controller class.
- Global Exception Handler – The handler methods in Global Controller Advice is similar to Controller based exception handler methods and are used when the controller class is not able to handle an exception. It is essential to provide exception handling for all pointcuts in an application, globally. Spring provides @ControllerAdvice annotation to define global exception handler to any class. The @ControllerAdvice annotation takes Exception class as it’s argument. This annotation was introduced in Spring 3.2. In accordance with it’s name, the @ControllerAdvice annotation is used to provide a single ExceptionHandler for multiple controllers.
- Custom Error Handler – The Spring Framework provides
ResponseEntityExceptionHandler
interface that can be implemented to create custom exception handler. The reason of having this additional custom exception handler is that Spring provides default classes that can be implemented in our spring bean configuration to get the benefits of exception handling. We can also override it to create our custom handler.
We will experiment with and explore these powerful annotations discussed above, which are provided by Spring to handle exceptions in our Spring boot application in the next half of this article.
Using @ResponseStatus annotation
The @ResponseStatus annotation can be used to configure the methods and classes with a status code that can be used to provide a HTTP response. With this annotation, we can specify the HTTP status code along with a message string to be sent to the client, whenever a specific type of exception occurs.
Let’s create a custom run-time exception to handle a situation when a book is not found. We need extend the java.lang.RuntimeException class and mark it with @ResponseStatus annotation.
1 2 3 4 |
@ResponseStatus(value = HttpStatus.ENTITY_NOT_FOUND, reason = "Book Not found") public class BookNotFoundException extends RuntimeException { } |
When this exception is caught, Spring uses the configuration that we have provided in @ResponseStatus annotation. Similarly, we can create a custom exception to handle when the isbn is of the book is invalid.
1 2 3 4 5 6 7 |
@GetMapping(value = "/book/{id}") public ResponseEntity<?> getBook(@PathVariable int id) { if (id < 0) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } return ResponseEntity.ok(user); } |
Using @ExceptionHandler and @RestControllerAdvice annotations
Let’s handle validation checks by customizing exception handling.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ValidationCheckException extends RuntimeException { private static final long serialVersionUID = 1L; private String msg; public ValidationException(String msg) { this.msg = msg; } public String getMsg() { return msg; } } |
@RestControllerAdvice is a new annotation included in Spring 3.2 + that can be used to provide a common ExceptionHandler code for multiple controllers. It is normally used along with @ExceptionHandler.
1 2 3 4 5 6 7 8 9 |
@RestControllerAdvice public class ExceptionHandler { @ResponseBody @ExceptionHandler(value = ValidationCheckException.class) public ResponseEntity<?> handleException(ValidationCheckException exception) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMsg()); } } |
Unlike @ResponseStatus annotation, we can additionally do things like notifying, log exceptions etc. by using this approach.
For example, if we want to update the id of an existing book. We have to check for 2 validation checks where the book’s ISBN id must be greater than 0 and the id must be between 20000 and 70000. Let’s create an endpoint having these validation checks in mind.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@PatchMapping(value = "/book/{id}") public ResponseEntity<?> updateId(@PathVariable int id, @RequestParam int id) { if (id < 0) { throw new ValidationCheckException("Id cannot be less than 0"); } if (id < 20000 || id > 70000) { throw new ValidationCheckException("Id must be between 20000 and 70000"); } Book book = findBook(id); book.setId(id); return ResponseEntity.accepted().body(book); } |
By default, @RestControllerAdvice is applicable to the application, globally.
Creating a custom global error handler:
The main goal of implementing a global error handler, customizing to our requirements is to give useful information about the error messages for the client. Using this, the client can easily diagnose the problem. Spring provides a base class named ResponseEntityExceptionHandler
to customize the exception handler. We can override the methods of this class to implement our custom exception handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@RestControllerAdvice public class CustomExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return errorResponse(HttpStatus.BAD_REQUEST, "Invalid method arguments:"); } private ResponseEntity<Object> errorResponse(HttpStatus status, String message) { return ResponseEntity.status(status).body(message); } } |
As you see above, we need to use the @ControllerAdvice
annotation. In the above example, we are overriding the MethodArgumentNotValid
method of the ResponseEntityExceptionHandler
to provide our custom implementation. There are many other useful methods provided by this class.
Conclusion
We discussed several methods to implement exception handling mechanism in Spring in this tutorial. While Spring boot does provide advanced error handling support out of the box, it does allow the developer to customize it to fit with their requirements. Hope this would help you in your projects. Have you used any other approach successfully in your projects to deal with errors/ exceptions ? Please leave your comment!