How do you handle exceptions globally in a Spring REST API (@ControllerAdvice/@ExceptionHandler)?
Quick Answer
@ExceptionHandler on a method inside a controller catches exceptions thrown by that controller's own handler methods, letting you return a specific, structured error response instead of letting the exception propagate to a generic error page. @ControllerAdvice (or @RestControllerAdvice for REST APIs) makes those @ExceptionHandler methods apply globally, across every controller in the application, centralizing consistent error-response formatting instead of duplicating try/catch logic in every endpoint.
Detailed Answer
Rather than wrapping every controller method in a try/catch, Spring MVC lets you declare exception-handling logic separately, and have it apply automatically wherever a matching exception is thrown.
@ExceptionHandler on a method inside a single controller — handles exceptions thrown by that controller's handler methods only:
@RestController
class OrderController {
@GetMapping("/orders/{id}")
Order getOrder(@PathVariable Long id) {
return repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
}
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<String> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
@RestControllerAdvice (or @ControllerAdvice + @ResponseBody) makes @ExceptionHandler methods apply globally, across every controller in the application — the standard way to centralize consistent error formatting instead of duplicating handling logic per-controller:
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ErrorResponse> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("ORDER_NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(Exception.class) // catch-all fallback
ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
}
}
Resolution order: Spring matches the most specific applicable @ExceptionHandler — a handler for the exact thrown exception type wins over a handler for one of its superclasses, and a handler defined on the specific controller itself (via a local @ExceptionHandler) takes precedence over a matching global @ControllerAdvice handler.
Practical benefits of centralizing this in a @RestControllerAdvice:
- Consistent error response shape across the entire API (a
code/message/timestampstructure, for example), which API consumers can rely on uniformly. - Avoids leaking internal exception details/stack traces to clients — the handler controls exactly what's exposed.
- Keeps controller methods focused on their actual logic, free of repetitive
try/catchboilerplate.
Spring Boot also ships a default fallback (ErrorController/BasicErrorController) that produces a generic JSON error body for any exception not otherwise handled — a @RestControllerAdvice typically exists specifically to produce something more specific and informative than that generic default.