Global Exceptions Handling
👉 Implementing @ControllerAdvice, @ExceptionHandler, and Custom Error Responses in Spring Boot for global Exception handling.
Error handling for APIs
Spring Boot provides a powerful and centralized mechanism for handling and customizing exceptions thrown by the application. This is typically achieved using the @ControllerAdvice annotation.
This approach decouples error handling logic from individual controller methods, leading to cleaner and more maintainable code.
The goal is to return a standardized, meaningful error response to the client instead of a raw exception stack trace.
Table of Contents
- Error handling for APIs
- Table of Contents
- Use Case
- Benefits
- Maven Dependency
- Implementation
- Exception Handling Flow
- Key Observations
- Best Practices
- Security Considerations
Use Case
The primary use case of Gloabl Exception handling is to provide a consistent and reliable API contract for error reporting.
When a resource is not found (404), an input is invalid (400), or an internal server error occurs (500), the client should receive a predictable JSON payload containing a timestamp, a clear error message, and request details.
Benefits
Centralized exception handling significantly improves API consistency and developer experience (DX) by eliminates redundant try-catch blocks in controllers, reduces boilerplate code, and ensures that clients always receive well-structured error messages, simplifying debugging and integration.
The use of custom exceptions clearly defines application-specific errors.
Features
- Centralized Handling:
- Use
@ControllerAdviceto globalize exception handling across all controllers.
- Use
- Exception Mapping:
- Use
@ExceptionHandlerto map specific exception classes to handler methods.
- Use
- Status Mapping:
- Use
@ResponseStatuson custom exceptions or set the status explicitly in theResponseEntity.
- Use
- Standardized Payload:
- Define a custom
ErrorDetailsbean to ensure a consistent structure for error responses.
- Define a custom
- Separation of Concerns:
- Keeps business logic and error handling distinctly separated.
Drawbacks
While beneficial, the setup involves initial overhead by requiring the creation of custom exception classes and the global handler.
Over-reliance on too many highly specific custom exceptions can lead to exception proliferation, making the code harder to navigate.
Moreover, incorrectly configuring the handler can unintentionally expose sensitive internal details.
Maven Dependency
The core functionalities for web applications, including exception handling via @ControllerAdvice and support for ResponseEntity, are provided as part of the spring-boot-starter-web dependency.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Implementation
ErrorDetails Bean
This class defines the standard structure for the error response payload sent back to the client. It typically includes temporal and context information.
import java.time.LocalDateTime;
public class ErrorDetails {
private LocalDateTime timestamp;
private String message;
private String details;
// Default Constructor
public ErrorDetails() {
}
// Parametrized Constructor
public ErrorDetails(LocalDateTime timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
// Constructor, setter-getters... (Assuming these are implemented for brevity)
// Getter and Setter methods for timestamp, message, and details should be present.
// Example Getter:
public LocalDateTime getTimestamp() {
return timestamp;
}
// Example Setter:
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}
UserNotFoundException
A custom exception class is used to clearly signal an application-specific error.
The @ResponseStatus(code = HttpStatus.NOT_FOUND) annotation is a convenient way to automatically map this exception to a specific HTTP status code, even without a @ControllerAdvice.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
private static final long serialVersionUID = 4882099180124262207L;
public UserNotFoundException(String message) {
super(message);
}
}
CustomizedResponseEntityExceptionHandler
The Global Exception handler monitors the exceptions across applications. The @ControllerAdvice or @RestControllerAdvice is the central component.
It uses @ExceptionHandler methods to intercept and process exceptions across the entire application, allowing us to contruct and return a standardized error response (ResponseEntity<ErrorDetails>) across application.
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
//@ControllerAdvice(basePackages = "com.srvivek.x.y.z") //for specific package
//@Order(value = 1) // for defining ordering
//@Priority(value = 1) // for defining ordering
//@RestControllerAdvice(basePackages = "com.srvivek.x.y.z") // @ControllerAdvice + @ResponseBody
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomizedResponseEntityExceptionHandler.class);
/**
* Generic exception handling (Fallback for all unhandled exceptions).
* Returns HTTP 500 INTERNAL_SERVER_ERROR.
* @param ex
* @param request
* @return
* @throws Exception
*/
@ExceptionHandler(value = Exception.class)
public final ResponseEntity<ErrorDetails> handleAllException(Exception ex, WebRequest request) throws Exception {
logger.error("Error stacktrace: {}", ex);
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Specific exception handling for UserNotFoundException.
* Returns HTTP 404 NOT_FOUND.
* @param ex
* @param request
* @return
* @throws Exception
*/
@ExceptionHandler(value = UserNotFoundException.class)
public final ResponseEntity<ErrorDetails> handleUserNotFoundException(Exception ex, WebRequest request)
throws Exception {
logger.error("Error stacktrace: {}", ex);
ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.NOT_FOUND);
}
}
Project POC:
Refer the application source code on Github: Exception Handing POC.
Exception Handling Flow
The diagram illustrates how an exception is captured and processed by the global handler before a standardized response is returned to the client.
sequenceDiagram
participant C as Client
participant A as Controller
participant S as Service
participant E as Exception
participant H as @ControllerAdvice
participant L as Logger
C->>A: API Request (e.g., GET /users/100)
A->>S: Call Service Method (findUser(100))
alt User Not Found
S-->E: Throws UserNotFoundException
E->>H: Exception Interception
H->>L: Log Error Stacktrace
H-->>C: Standardized Error Response (404 NOT FOUND)
else User Found
S-->>A: User Object
A-->>C: HTTP 200 OK Response
end
Key Observations
| Feature | Description | Example / Annotation |
|---|---|---|
Scoping | Allows the exception handler to apply only to controllers within specific packages, providing modularity. | @ControllerAdvice(basePackages = "com.srvivek.x.y.z") |
Ordering | Defines the precedence when multiple @ControllerAdvice classes exist, ensuring predictable exception resolution. | @Order(value = 1) or @Priority(value = 1) |
Logging | It is critical to explicitly log the exception within the handler, as Spring Boot doesn’t automatically log the intercepted exception’s details. | logger.error("Error stacktrace: {}", ex); |
Convenience | A specialized annotation that is equivalent to combining @ControllerAdvice and @ResponseBody, streamlining REST API development. | @RestControllerAdvice |
Best Practices
| Practice | Description | Key Action / Benefit |
|---|---|---|
Log Explicitly | Always ensure the full exception stack trace is captured and recorded on the server side. | Use logger.error(ex.getMessage(), ex); in the handler method. |
Specific to Generic | Structure your @ExceptionHandler methods to catch the most specific exceptions first, with a fallback to the generic Exception.class. | Ensures the correct HTTP status and custom message is returned for known errors. |
Avoid Exposing Details | Never include sensitive server-side information (like stack traces or internal paths) in the API error response sent to the client. | Sanitize the error message and use a simple details field. |
Leverage Spring's Built-in | Extend ResponseEntityExceptionHandler to customize handling of common Spring exceptions (e.g., validation failures, method not allowed). | Provides easy overrides for exceptions like MethodArgumentNotValidException. |
Use Problem Detail Standard | Adopt the RFC 7807 standard (type, title, status, detail, instance) for your error response structure. | Enhances API maturity, consistency, and client-side error processing. |
Security Considerations
- No Stack Traces in Response: Ensure the client-facing error message never contains server-side stack traces, internal configuration, or sensitive class names.
- Generic Messages for 5xx: For
500 INTERNAL_SERVER_ERRORor other unexpected errors, provide a generic message like “An unexpected error occurred” and use a unique correlation ID (logged server-side) that the client can reference when reporting the issue. - Input Validation: Implement proper input validation before the exception handler to prevent common injection attacks or unexpected behaviour.