Skip to content
Spring Boot sb exceptions 3 min read

@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 a 500 — it can leak internals.

@RestControllerAdvice vs @ControllerAdvice

AnnotationEquivalent toReturns
@ControllerAdviceA view name unless methods add @ResponseBody
@RestControllerAdvice@ControllerAdvice + @ResponseBodyJSON/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 { }
AttributeScope
basePackages / basePackageClassesControllers in those packages
assignableTypesSpecific controller classes
annotationsControllers 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, ResponseEntityExceptionHandler produces RFC 7807 ProblemDetail bodies by default. Override the relevant handle* methods (or the createResponseEntity hook) if you need your own shape, as above. See Handling Validation Errors.

Pitfalls

  • Forgetting @RestControllerAdvice (using plain @ControllerAdvice) without @ResponseBody makes 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 ResponseEntityExceptionHandler method but returning null suppresses the response — return a ResponseEntity.
Last updated June 13, 2026
Was this helpful?