Exception Handling — Spring Boot REST API

Amila Iroshan
The Fresh Writes
Published in
8 min readJan 27, 2024

The purpose of this article is how we can configure error/exception handling of a Spring Boot application to bring any error inside the system to the end-user. Spring Boot offers more than one way of doing it. This document will explore these ways and will also provide some pointers on when one given way might be preferable over another.

1). Introduction to Error Handling

Exception handling is one of the core concepts that is really necessary when we build a robust application using Spring Boot. Therefore, in REST API development, it carries a bigger role.

2). What is Error/Exception

In brief, an exception is a Java object that is thrown at run time during the execution of a program that disrupts the normal flow of instructions. Spring Boot provides us libraries/classes to handle exceptions beyond simple ‘try-catch’ blocks. To use these approaches, we apply a couple of annotations that allow us to treat exception handling as a cross-cutting concern.

3). Spring Boot Default Exception Handling Mechanism

REST applications developed in Spring Boot automatically take advantage of its default error-handling logic. Specifically, whenever an error occurs, a default response containing some information is returned. The problem is that this information may be poor or insufficient for the API callers to deal with the error properly.

By default, when no valid mappings can be found, Spring Boot automatically configures a default fallback error page, as well as an error-handling response in case of REST requests.

Figure 1. The Spring Boot default white label HTML Error Page and 500 Internal Server error response.

4). Custom Exception Handling in Spring Boot

4.1).Before Spring 3.2 , spring introduced @ExceptionHandler annotation to handle exception.

Pros

· @ExceptionHandler allows you to define custom error-handling logic for specific exceptions or groups of exceptions.

· You can control how exceptions are handled, log error information, and generate user-friendly error responses.

· Separating error-handling logic into dedicated methods (annotated with @ExceptionHandler) keeps your controller methods cleaner and focused on their primary responsibilities.

Cons

· @ExceptionHandler promotes cleaner code in many cases, it can also lead to increased complexity when dealing with a large number of exception types.

· If not carefully managed, some exceptions might be overlooked or handled inappropriately. This can result in unexpected behavior or security vulnerabilities in your application.

· Depending on the number of exception types and the complexity of your error responses, you may end up writing boilerplate code for handling exceptions and constructing error responses.

Figure 2. Code Snippet of usage of @ExceptionHandler

This approach has a major drawback: The @ExceptionHandler annotated method is only active for that particular Controller, not globally for the entire application.

To overcome such drawbacks Since Spring 3.2 they introduced new solution called @ControllerAdvice annotation.

4.2). The @ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers into a single, global error-handling component.

· It gives us full control over the body of the response as well as the status code.

· It provides mapping of several exceptions to the same method, to be handled together.

· It makes good use of the newer RESTful ResposeEntity response.

· It provides global point of exception handle over the application rather than class level.

Figure 3. Code Snippet of usage of @ControllerAdvice

Besides that spring introduced @ResponseStatus annotation, which 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

This annotation allows you to customize the HTTP response status for specific exceptions, providing more control over how errors are communicated to clients.

Ex: @ResponseStatus(value = HttpStatus.NOT_FOUND)

4.3). ResponseStatusException (Spring 5 and Above)

Spring 5 introduced the ResponseStatusException class and it provides ,

· Excellent approach for prototyping: We can implement a basic solution quite fast.

· One type, multiple status codes: One exception type can lead to multiple different responses.

· We won’t have to create as many custom exception classes.

But ResponseStatusExceptions class operates class level and it may resulting in Ccode duplication: We may find ourselves replicating code in multiple controllers.

@GetMapping(value = "/{id}") 
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
try {
Foo resourceById = RestPreconditions.checkFound(service.findOne(id));
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
return resourceById;
}
catch (MyResourceNotFoundException exc) {
throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Foo Not Found", exc);
} }

5). Best Practices for Exception Handling in Spring Boot

5.1). Use informative error messages

It is important to provide a clear and descriptive error message that explains the cause of the exception. This will help developers quickly identify and resolve the issue.

5.2). Use HTTP status codes

Spring Boot provides built-in support for mapping exceptions to HTTP status codes. Use these status codes to provide a clear indication of the nature of the exception.

5.3). Use @ExceptionHandler

Spring Boot provides the @ExceptionHandler annotation to handle exceptions thrown by a specific controller method. This annotation can be used to provide customized error responses for specific exceptions.

5.4). Use @ControllerAdvice

The @ControllerAdvice annotation to handle exceptions globally across all controllers. This annotation can be used to provide a centralized error handling mechanism for an entire application.

5.5). Use loggers

Use a logger to record details of the exception, including the stack trace, timestamp, and any relevant context information.

5.6). Use custom exceptions

Spring Boot allows you to define your own custom exceptions. Use these exceptions to provide more specific error messages and to handle unique exceptions that may not be covered by built-in Spring Boot exceptions.

5.7) Use error codes

In addition to HTTP status codes, it can be useful to define your own error codes to provide additional information about the cause of an exception. These error codes can be included in the response body or in the logs, making it easier to diagnose and fix issues. These error codes may be domain specific error codes and then developer can easily understand.

5.8). Use validation

Validating input data is an important part of preventing exceptions. Use validation annotations to ensure that input data is valid before processing it. This can help to prevent exceptions from occurring in the first place.

6). Implement Exception Handling in Spring Boot

6.1 Initializing the Project

Use the Spring Initializr with the following settings to create the initial application.

· Project: Maven

· Language: Java

· Packaging: Jar

· Java: 17

· Spring Boot: 3.1.x (e.g., 3.1.3)

· Dependencies

# Spring Data JPA

# Spring Web

# MySQL JDBC Driver

# Lombok

# Modelmapper(v3.1.0)

# Spring Validation

# Spring Devtools

Leave the rest of the settings as it is. Click “Generate” to download the project zip file. Extract it and open it with your IDE.

6.2 Project Structure

Figure 4. Project Structure

6.3 Defining Data Source

6.3.1 Configure application.properties file with database configurations.

Figure 5. Data Base Configuration

6.3.2 Add entity class called Supplier, which is map with the relation DB.

Figure 6. Entity Class

6.3.3 Added the sample data to data.sql, which is loads the data when bootstrapping the application.

Figure 7. data.sql file

6.4 Define Rest Controller

Figure 8. Rest Controller class

Define four rest endpoints for demo purposes. I have considered four exception scenarios.

· Get data from db which does not exist on db and throws a custom exception called NoSuchSupplierExistsException.

· Add supplier to db, which supplier already exists on db and throws custom exception called SupplierAlreadyExistsException.

· Use validation at SupplierInputDto class and when onboarding supplier checks validation rules and throws MethodArgumentNotValidException.

· Call rest api using invalid http request method and then it throws exception HttpRequestMethodNotSupportedException.

6.5 Define Global Exception Handler class.

Figure 9. Global_Exception_Handler class

6.6 Define custom exception class.

Figure 10. Custom Exception class

6.7 Define common error response class.

Figure 11. Error Response class

6.8 Define domain specific error messages

Figure 12. Error Code class

6.9 Define DTO class with validation.

Figure 13. Supplier Input DTO class

7). Run the project and test output.

Use Maven on terminal or your IDEs run configuration to start the application.

7.1 Testing Application using postman rest client.

7.1.1 Test Result 01 — Get Supplier who does not exist with system

Figure 14. Test Result 01

7.1.2 Test Result 02 — Onboard Supplier who is already exists on the system

Figure 15. Test Result 02

7.1.3 Test Result 03 — Onboard Supplier who is violate validation rules

Figure 16. Test Result 03

7.1.4 Test Result 04 — Send request with invalid http method

Figure 17. Test Result 04

8). Implement unit testing for GlobalExceptionHandler

@SpringBootTest
public class GlobalExceptionHandlerTest {
@InjectMocks
private GlobalExceptionHandler globalExceptionHandler;
private ErrorResponse errorResponse;
@Autowired
private SupplierService supplierService;
@BeforeEach
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void testHandleResourceNotFoundException(){
// Create a sample ResourceNotFoundException
ResourceNotFoundException ex=new ResourceNotFoundException(ErrorMessages.ERROR_SUPPLIER_NOT_FOUND + 12);
// Define the expected response
ResponseEntity<ErrorResponse> expectedResponse = ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND.value(),
ex.getMessage()));
//Define Actual response
ResponseEntity<ErrorResponse> actualResponse = globalExceptionHandler.handleResourceNotFoundException(ex);
// Call the method and assert the response
assertEquals(expectedResponse.getStatusCode(), actualResponse.getStatusCode());
assertEquals(expectedResponse.getBody().getMessage(), actualResponse.getBody().getMessage());
}
@Test
public void testHandleSupplierExistsException(){
// Create a sample ResourceNotFoundException
SupplierAlreadyExistsException ex=new SupplierAlreadyExistsException(ErrorMessages.ERROR_SUPPLIER_ALREADY_FOUND + 10);

Conclusion

In this article, we’ve explored Spring Boot’s error-handling mechanisms step-by-step, backed by practical examples. By leveraging the power of @ControllerAdvice and @ExceptionHandler, we can efficiently handle exceptions and provide meaningful responses to users. Exception handling deals with unexpected situations in a friendly way. Instead of crashing the entire application, the application will show useful messages to users, making the application more user-friendly and reliable.

You can find the complete code for this example on my GitHub

Thank you for read this article and If you like this article, do follow and clap 👏🏻.Happy coding, Cheers !!😊😊

--

--

Amila Iroshan
The Fresh Writes

Software Engineer | Open Source Contributor | Tech Enthusiast