@RestControllerAdvice
@RestControllerAdvice is a specialization of @ControllerAdvice that combines it with @ResponseBody, so handler return values are serialized to the response body. A single advice bean applies its @ExceptionHandler methods to every controller in the application — making it the natural home for an API-wide error policy. This page covers defining a global handler, ordering, scoping, and extending the framework’s base handler.
A global exception handler
Annotate a class with @RestControllerAdvice and add @ExceptionHandler methods. They behave exactly like controller-local handlers but apply across all controllers.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError notFound(ResourceNotFoundException ex, HttpServletRequest req) {
return ApiError.of(404, "NOT_FOUND", ex.getMessage(), req.getRequestURI());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError badRequest(IllegalArgumentException ex, HttpServletRequest req) {
return ApiError.of(400, "BAD_REQUEST", ex.getMessage(), req.getRequestURI());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError unexpected(Exception ex, HttpServletRequest req) {
log.error("Unhandled exception", ex); // log full detail
return ApiError.of(500, "INTERNAL_ERROR",
"An unexpected error occurred", req.getRequestURI());
}
}
Tip: Always log unexpected exceptions with the full stack trace server-side, but return a generic message to the client. Never echo
ex.getMessage()for a500— it can leak internals.
@RestControllerAdvice vs @ControllerAdvice
| Annotation | Equivalent to | Returns |
|---|---|---|
@ControllerAdvice | — | A view name unless methods add @ResponseBody |
@RestControllerAdvice | @ControllerAdvice + @ResponseBody | JSON/serialized body (use this for REST APIs) |
For REST APIs, always use @RestControllerAdvice so your error DTOs are serialized rather than treated as view names.
Matching precedence and @Order
Within one advice class, Spring picks the most specific @ExceptionHandler for the thrown type — so a ResourceNotFoundException handler beats the Exception catch-all. When you have multiple advice beans, their relative priority is controlled with @Order (lower value = higher priority).
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // consulted first
public class SecurityExceptionHandler { /* auth/access errors */ }
@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE) // catch-all consulted last
public class GlobalExceptionHandler { /* everything else */ }
Note: Put broad catch-alls (
Exception.class) in the lowest-precedence advice so more specific advices get a chance to handle their exceptions first.
Scoping an advice
By default an advice applies globally. You can narrow it to a subset of controllers using attributes on the annotation — useful when a module (e.g. an admin API) needs different error shapes.
// Only controllers in the given base package(s)
@RestControllerAdvice(basePackages = "com.example.admin")
public class AdminExceptionHandler { }
// Only controllers assignable to a type
@RestControllerAdvice(assignableTypes = { OrderController.class, CartController.class })
public class CheckoutExceptionHandler { }
// Only controllers carrying a specific annotation
@RestControllerAdvice(annotations = PublicApi.class)
public class PublicApiExceptionHandler { }
| Attribute | Scope |
|---|---|
basePackages / basePackageClasses | Controllers in those packages |
assignableTypes | Specific controller classes |
annotations | Controllers annotated with the given annotation |
Extending ResponseEntityExceptionHandler
Spring MVC throws many built-in exceptions — MethodArgumentNotValidException (bean validation), HttpMessageNotReadableException (malformed JSON), HttpRequestMethodNotSupportedException, NoResourceFoundException, and more. ResponseEntityExceptionHandler is an abstract base that already maps all of them. Extend it to customize those responses while keeping the defaults.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Override to reshape validation errors into your DTO
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatusCode status, WebRequest request) {
List<FieldError> fields = ex.getBindingResult().getFieldErrors().stream()
.map(f -> new FieldError(f.getField(), f.getDefaultMessage()))
.toList();
ApiError body = ApiError.of(400, "VALIDATION_FAILED",
"Request validation failed", path(request));
body.setFieldErrors(fields);
return ResponseEntity.badRequest().body(body);
}
// Your own domain handlers live alongside the overrides
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> notFound(ResourceNotFoundException ex, WebRequest req) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiError.of(404, "NOT_FOUND", ex.getMessage(), path(req)));
}
private String path(WebRequest req) {
return ((ServletWebRequest) req).getRequest().getRequestURI();
}
}
Output (malformed JSON body):
{
"status": 400,
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"path": "/api/products",
"fieldErrors": [
{ "field": "price", "message": "must be greater than 0" }
]
}
Note: Since Spring Framework 6,
ResponseEntityExceptionHandlerproduces RFC 7807ProblemDetailbodies by default. Override the relevanthandle*methods (or thecreateResponseEntityhook) if you need your own shape, as above. See Handling Validation Errors.
Pitfalls
- Forgetting
@RestControllerAdvice(using plain@ControllerAdvice) without@ResponseBodymakes Spring treat your returned DTO as a view name →500. - Two advices that both match the same exception with equal specificity: order is undefined unless you set
@Order. - Overriding a
ResponseEntityExceptionHandlermethod but returningnullsuppresses the response — return aResponseEntity.