Spring Boot: Exception Handling Best Practices

Use @ControllerAdvice for global exception handling

The Java Trail
5 min readSep 3, 2023

@ControllerAdvice is a global exception handling mechanism in Spring. It allows you to define a central place for handling exceptions that occur in your application.

When an exception is thrown from any controller method (e.g., a REST endpoint), it is caught by the @ControllerAdvice annotated class.

The @ExceptionHandler methods within the @ControllerAdvice class handle specific exception types and return appropriate responses.

Let’s provide a more detailed explanation for the real-life example of throwing custom exceptions from a service layer and handling them globally in a Spring Boot application for a product-related scenario.
Steps:

1. Create a Custom ProductNotFoundException:

In this step, we create a custom exception class, ProductNotFoundException, which extends RuntimeException. This custom exception is used to represent situations where a product is not found in the system. By creating a custom exception, we can provide more specific information about the error.

public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long productId) {
super(“Product not found with ID: “ + productId);
}
}

Explanation:

  • Custom exceptions are used to capture application-specific error scenarios.
  • In this case, we include the productId in the exception message to provide detailed information about the missing product.

2. Create a Product Service:

Here, we create a ProductService class responsible for fetching product information. If a product is not found, it throws a ProductNotFoundException.

@Service
public class ProductService {

public Product getProductById(Long productId) {
// Simulate product retrieval logic
Product product = getProductFromDatabase(productId);

if (product == null) {
throw new ProductNotFoundException(productId);
}

return product;
}

// Simulated method to fetch a product from a database
private Product getProductFromDatabase(Long productId) {
// Implement your database logic here
// Return null if the product is not found
return null;
}
}

Explanation:

  • The ProductService encapsulates the business logic related to products.
  • The getProductById method simulates fetching a product from a database or another data source.
  • If the product is not found (based on the simulation), it throws a ProductNotFoundException.

3. Create a Global Exception Handler:

This step involves creating a global exception handler using @ControllerAdvice. The handler is responsible for catching ProductNotFoundException globally and returning a custom error response.

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}

Explanation:

  • @ControllerAdvice marks the class as a global exception handler, allowing it to handle exceptions from multiple controllers.
  • The @ExceptionHandler method within the handler catches ProductNotFoundException.
  • It constructs an ErrorResponse object with a 404 Not Found status code and the custom error message from the exception.
  • The handler returns a JSON response containing the error details.

4. Create a Custom Error Response Class:

We define a custom error response class, ErrorResponse, to structure error messages consistently in our application.

public class ErrorResponse {
private int statusCode;
private String message;

public ErrorResponse(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
// Getter methods
}

Explanation:

  • The ErrorResponse class provides a standardized format for error responses.
  • It includes fields for the HTTP status code and a message describing the error.

5. Controller to Fetch Product:

In this step, we create a controller, ProductController, responsible for handling requests to fetch products by their IDs

@RestController
@RequestMapping("/api/products")
public class ProductController {

@Autowired
private ProductService productService;

@GetMapping("/{productId}")
public ResponseEntity<Product> getProduct(@PathVariable Long productId) {
Product product = productService.getProductById(productId);
return ResponseEntity.ok(product);
}
}

Explanation:

  • The ProductController defines an endpoint to retrieve product details by the product's ID.
  • It uses the ProductService to fetch the product.
  • If the product is found, it returns a successful response with the product data.

6. Testing the Exception Handling:

To test the exception handling, make a GET request to fetch a product with an ID that doesn’t exist in the system. This will trigger the ProductNotFoundException, and the global exception handler will respond with a JSON error message.

curl -X GET http://localhost:8080/api/products/123

Response

{
"statusCode": 404,
"message": "Product not found with ID: 123"
}

Explanation:

  • The test request attempts to fetch a product with an ID (e.g., 123) that doesn’t exist in the system.
  • As a result, the ProductNotFoundException is thrown from the service layer.
  • The global exception handler, defined with @ControllerAdvice, captures the exception and constructs a JSON error response with a 404 status code and the custom error message.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Sequence Flow Diagram

the flow sequence diagram from the controller to error handling using @ControllerAdvice in a Spring application:

  1. Controller Layer:
  • A client (e.g., a web browser or a REST client) makes a request to a specific controller method, typically an HTTP endpoint exposed by your Spring application.

2. Controller Method Execution:

  • The Spring MVC framework routes the incoming request to the appropriate controller method based on the request mapping annotations, such as @GetMapping or @PostMapping.
  • The controller method executes and performs its business logic.

3. Exception Occurs:

  • During the execution of the controller method, an exception can be thrown for various reasons. This could be due to business logic errors, validation failures, or any other unexpected issues.

4. Exception Propagation:

  • Once an exception is thrown within the controller method, it starts propagating up the call stack.

5. @ControllerAdvice Class:

  • The exception propagates up until it reaches the global exception handler class annotated with @ControllerAdvice.

6. @ExceptionHandler Methods:

  • Within the @ControllerAdvice class, there are one or more @ExceptionHandler methods defined to handle specific types of exceptions.

7. Matching Exception Handler:

  • The Spring framework identifies the appropriate @ExceptionHandler method to handle the specific exception type based on the method's parameter type.

8. Exception Handling:

  • The matched @ExceptionHandler method executes to handle the exception.
  • This method can perform tasks such as logging the error, constructing an error response, or taking any other custom action.

9. Response Generation:

  • The @ExceptionHandler method typically generates an error response, which can be in the form of a JSON response, an HTML page, or any other response format.

10. Response Sent to Client:

  • The error response generated by the @ExceptionHandler method is sent back to the client that made the original request.

11. Client Receives Error Response:

  • The client receives the error response and can process the error information as needed. For example, it can display an error message to the user or handle the error programmatically.

--

--

The Java Trail

Scalable Distributed System, Backend Performance Optimization, Java Enthusiast. (mazumder.dip.auvi@gmail.com Or, +8801741240520)