Unexpected situations may arise in any software application or system when the program goes south. At this crucial juncture, exception handling comes into play.
Exception handling is a very crucial part of the application, it takes care of defining the steps to be taken in case of any unforeseen circumstances at the time of execution. While working on API development, we need to clearly define what a client or a caller does if the API does not respond as expected. The API response, in this case, should be meaningful enough so that the client can take corrective action.
In this article, we are going to look at how this is done for REST APIs in Spring Boot with the help of ControllerAdvice and ExceptionHandler annotations. These annotations help us define a global component that acts as an interceptor to intercept all the exceptions across all the handlers in the application and handle them at one central place, separating it from the rest of the application logic and avoiding duplication of exception handling code.
Demo Application
Let us consider a simple employee management application with two REST APIs exposed.
- GET request: To fetch an employee by id from DB
- POST request: Save the employee data to DB
Controller Class
A controller class processes incoming REST API requests, prepares a model and returns the view as a response.
Service Class
Service class ensures class files are used to write business logic in a different layer and are separated from RestController class file.
Repository Class
Repository Class indicates the class providing mechanism for storage, retrieval, search, update and delete operation on objects.
Entity Class
Entity class defines that the class is an entity and is appropriately mapped to the database table.
Sample Requests
When we send the requests to these two APIs with valid data, output looks like below:
[POST] Create Employee
[GET] Fetch Employee
Let us now invoke these APIs with invalid data.
[POST] Save Employee with Null ID
[GET] Fetch Employee with Invalid ID
Both responses, in case of invalid request data, look cryptic and not something we may want to send to the user.
Spring Boot provides us a way where we can handle these kinds of exception scenarios, by intercepting the exception before they are sent to the user and stub meaningful response and send relevant details to the user.
Let us look at two Spring Boot annotations that would help us do this:
- @ControllerAdvice: This annotation was introduced in Spring 3.2 version and is used to handle the exceptions globally, we will create a class annotated with this, with handler methods handling each type of exception.
- @ExceptionHandler: In the class annotated with @ControllerAdvice, we will have handler methods for each type of exception that may occur in an application. These methods would construct the user-friendly response and send it back to the user.
Demo Application Modifications to Handle Invalid GET Request
Custom Exception Class
We have created a custom exception class as below to represent an exception thrown when the employee, we are trying to retrieve, is not found in the DB.
Updated Service Class
We will modify service implementation to get to throw this exception when there is no data found.
This exception would be caught by a method annotated with the handler method in the global API exception handler and the custom response would be sent back.
Global Exception Handler Class
We will create a new class and it will be annotated with @ControllerAdvice to intercept the exceptions and handle them centrally in one place.
This class would have handler methods for all the exceptions we want to catch in the handler code and would create a custom response stubbing the details relevant to the user and send it back. In this case, we will send the exception message, next steps to take, and timestamp when it occurred.
Custom Response Class
Custom response would look like below, we can enhance this as needed to send the response fields we want.
After the above changes, when we hit the same request with invalid data, the response looks much simpler and something which can help a user understand what has gone wrong in the API call.
[GET] Fetch Employee with Invalid ID
Changes to Demo Application to Handle REST API Validations
We can also use Global API Exception Handler to customize the response sent as part of inbuilt Rest API constraint validations when any of the validation fails. For e.g., will add @NotNull validation on the id field as below.
DTO Class with Constraint Validation Added
If we do not catch this in the global API handler, the default response looks like below when the validation fails, which is not very helpful to the user.
[POST] Save Employee with Null ID
Global API Exception Handler Changes for Constraint Validation Failure
We will add a custom API handler method for the “MethodArgumentNotValidException” exception which is thrown when any of the constraint validation fails.
After this is added, when we hit the save API with invalid data the response looks below which sends the custom response we stubbed in the handler.
[POST] Save Employee with Null ID
Best Practices of Exception Handling in Spring Boot
- Create custom exceptions for specific cases with dedicated handlers in the Global API Exception Handler.
- Even if we are catching the exception in the handler, we should make sure that the exception stack trace is logged in server logs with proper severity to make debugging easier.
- Refrain from sending the stack trace of the exception to the user.
- If there is a need to use custom application error codes, choose a range that does not collide with existing HTTP error codes.
Way Forward
Software engineering teams can use Global API Exception Handler to globally handle exceptions for REST APIs. In addition, we can add custom application-specific error codes and stub in custom response and send back. Handling exceptions globally makes it easy to decouple the exception handling logic at one place keeping it separate from the rest of the application code.
Looking to make your engineering teams’ delivery excellent and effective?