Exception Handling — @ControllerAdvice
Intermediate@ControllerAdvice combined with @ExceptionHandler centralises error handling across all controllers, returning consistent error response DTOs.
Overview
Without centralised exception handling, every controller would need its own try-catch blocks and ad-hoc error responses — leading to inconsistent HTTP status codes, leaked stack traces, and duplicated code. @ControllerAdvice (or @RestControllerAdvice, which adds @ResponseBody) is a cross-cutting component that intercepts exceptions thrown by any @Controller in the application and maps them to well-structured error responses. Combined with @ExceptionHandler methods, you get a single place to define every error scenario: validation failures return 400 with field-level detail, entity-not-found returns 404, and unexpected exceptions return 500 — all with a consistent JSON shape that your API clients can rely on.
Building a Global Exception Handler
Annotate a class with @RestControllerAdvice to make it the single source of truth for error handling. Each @ExceptionHandler method declares which exception type(s) it handles. The return type is your error DTO — keep it consistent across all handlers so clients parse one shape. Use @ResponseStatus or ResponseEntity to control the HTTP status code.
// Consistent error response DTO
@Getter
@Builder
public class ApiError {
private int status;
private String error;
private String message;
private Instant timestamp;
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
// 404 — Resource not found
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError handleNotFound(EntityNotFoundException ex) {
return ApiError.builder()
.status(404)
.error("Not Found")
.message(ex.getMessage())
.timestamp(Instant.now())
.build();
}
// 409 — Business rule violation
@ExceptionHandler(DuplicateResourceException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiError handleConflict(DuplicateResourceException ex) {
return ApiError.builder()
.status(409).error("Conflict").message(ex.getMessage())
.timestamp(Instant.now()).build();
}
// 500 — Catch-all for unexpected errors
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleAll(Exception ex) {
log.error("Unhandled exception", ex);
return ApiError.builder()
.status(500).error("Internal Server Error")
.message("An unexpected error occurred")
.timestamp(Instant.now()).build();
}
}Handling Bean Validation Errors
When @Valid fails on a @RequestBody parameter, Spring throws MethodArgumentNotValidException. Handle it in @RestControllerAdvice to extract per-field error messages and return them in the 400 response body. This gives API consumers a clear map of which fields failed and why.
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiValidationError handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
(a, b) -> a // keep first message if multiple violations on same field
));
return ApiValidationError.builder()
.status(400)
.error("Validation Failed")
.fieldErrors(fieldErrors) // e.g. {"email": "must be a valid email address"}
.timestamp(Instant.now())
.build();
}
// Also handle path variable / query param type mismatches
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
return ApiError.builder()
.status(400).error("Bad Request")
.message("Invalid value for parameter: " + ex.getName())
.timestamp(Instant.now()).build();
}Custom Business Exceptions
Define a hierarchy of application-specific unchecked exceptions. Throw them from service layer; let @RestControllerAdvice handle the translation to HTTP. This keeps service layer code clean (no HttpStatus references) and makes the mapping explicit and testable.
// Custom exception hierarchy
public class ApplicationException extends RuntimeException {
public ApplicationException(String message) { super(message); }
}
public class EntityNotFoundException extends ApplicationException {
public EntityNotFoundException(String entity, Object id) {
super(entity + " not found with id: " + id);
}
}
public class InsufficientStockException extends ApplicationException {
public InsufficientStockException(Long productId, int available) {
super("Product " + productId + " has only " + available + " units in stock");
}
}
// Service — throws business exception, no HTTP knowledge
@Service
public class OrderService {
public Order getOrder(Long id) {
return orderRepo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Order", id));
}
}Key Points to Remember
- 1@RestControllerAdvice centralises all exception-to-response mapping in one class, eliminating try-catch in every controller.
- 2Return a consistent error DTO (status, error, message, timestamp) so all API consumers can parse errors uniformly.
- 3MethodArgumentNotValidException carries per-field validation errors — extract and return field-level messages in a 400 response.
- 4Define a custom exception hierarchy in the service layer; never reference HttpStatus there — keep HTTP concerns in the advice class.
- 5Always have a catch-all @ExceptionHandler(Exception.class) as the last resort to prevent stack traces leaking to clients.
- 6@ResponseStatus on the method is convenient; use ResponseEntity<ApiError> when you need to set response headers dynamically.
Interview Questions
Sign in to ask AriaHow do you implement global exception handling in a Spring Boot REST application?
What is the difference between @ControllerAdvice and @RestControllerAdvice?
How do you return field-level validation errors in a Spring Boot API when @Valid fails?
Where should you throw exceptions — in the controller or service layer? Why?
How do you prevent a Spring Boot application from leaking internal stack traces to API clients?
Ask Aria about Exception Handling — @ControllerAdvice
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.