Module: Exception Handling

Global Error Handling

Global Exception Handling in Spring Boot

So far, we've seen how to handle exceptions within specific controller methods. But what if we want a centralized way to manage errors across our entire application? That's where Global Exception Handling comes in. It allows us to define a consistent error response format and avoid repetitive error handling code in each controller.

Why Global Exception Handling?

  • Centralized Error Management: Handle all exceptions in one place, making maintenance easier.
  • Consistent Error Responses: Provide a uniform error format to clients, improving the developer experience.
  • Reduced Code Duplication: Avoid repeating the same error handling logic in multiple controllers.
  • Improved Readability: Controllers become cleaner and focus on business logic.
  • Security: Control what information is exposed to the client in error responses.

Implementing Global Exception Handling

We achieve global exception handling using the @ControllerAdvice annotation. This annotation tells Spring that a class is responsible for handling exceptions thrown by controllers.

1. Create an Exception Handling Class:

Let's create a class called GlobalExceptionHandler.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    // ... (Exception handling methods will go here) ...
}
  • @ControllerAdvice: Marks this class as a controller advice component.
  • extends ResponseEntityExceptionHandler: Extending this class provides default exception handling for common Spring exceptions (like MethodArgumentNotValidException). We can override these defaults if needed.

2. Define Exception Handling Methods:

Inside GlobalExceptionHandler, we define methods annotated with @ExceptionHandler. These methods will be invoked when the specified exception type is thrown within a controller.

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleResourceNotFound(ResourceNotFoundException ex) {
    Map<String, String> errorMap = new HashMap<>();
    errorMap.put("error", "Resource Not Found");
    errorMap.put("message", ex.getMessage());
    return new ResponseEntity<>(errorMap, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
    Map<String, String> errorMap = new HashMap<>();
    errorMap.put("error", "Invalid Argument");
    errorMap.put("message", ex.getMessage());
    return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class) // Catch-all for other exceptions
public ResponseEntity<Map<String, String>> handleGlobalException(Exception ex) {
    Map<String, String> errorMap = new HashMap<>();
    errorMap.put("error", "Internal Server Error");
    errorMap.put("message", "An unexpected error occurred.");
    return new ResponseEntity<>(errorMap, HttpStatus.INTERNAL_SERVER_ERROR);
}
  • @ExceptionHandler(ExceptionType.class): Specifies the exception type this method handles.
  • ResponseEntity<Map<String, String>>: We return a ResponseEntity to control the HTTP status code and the response body. The response body is a Map containing error details. You can customize this to use a more complex error object if desired.
  • HttpStatus: Sets the appropriate HTTP status code for the error.
  • ex.getMessage(): Retrieves the exception message. Be careful about exposing sensitive information in the message.

3. Custom Exception Classes (Optional but Recommended):

For better organization and clarity, it's good practice to define custom exception classes. Let's create a ResourceNotFoundException:

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

4. Using the Custom Exception in a Controller:

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

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        // Simulate a resource not found scenario
        if (id == 1) {
            throw new ResourceNotFoundException("Product with ID 1 not found");
        }
        return new Product(id, "Example Product");
    }
}

5. Testing the Global Exception Handling:

Now, if you send a request to /products/1, the ResourceNotFoundException will be thrown. The GlobalExceptionHandler will catch it, and return a JSON response like this:

{
  "error": "Resource Not Found",
  "message": "Product with ID 1 not found"
}

with an HTTP status code of 404 (NOT_FOUND).

Key Considerations:

  • Exception Hierarchy: Order your @ExceptionHandler methods from specific to general. The most specific exception type should be handled first. The Exception.class handler should be last as a catch-all.
  • Logging: Always log exceptions for debugging and monitoring purposes. Use a logging framework like Logback or Log4j.
  • Security: Avoid exposing sensitive information in error messages. Consider using generic error messages for production environments.
  • Error Response Format: Design a consistent error response format (e.g., JSON with error, message, code fields) for your API.
  • ResponseEntityExceptionHandler: Leverage the built-in exception handling provided by ResponseEntityExceptionHandler for common Spring exceptions. Override methods as needed to customize the behavior.
  • @Valid and Exception Handling: When using @Valid for request body validation, MethodArgumentNotValidException is thrown. ResponseEntityExceptionHandler provides a default handler for this, but you can override it in your GlobalExceptionHandler to customize the error response.

This provides a solid foundation for implementing global exception handling in your Spring Boot applications. Remember to tailor the error responses and logging to your specific application requirements.