Loading Now

Spring Boot ControllerAdvice: A Complete, Practical Guide

spring-boot-controller-advice

Spring Boot ControllerAdvice: A Complete, Practical Guide

What Is @ControllerAdvice?

@ControllerAdvice is Springโ€™s mechanism for applying cross-cutting concerns to multiple controllersโ€”primarily global exception handling, but also data binding and model attributes. It lets you:

  • Centralize how exceptions are translated into HTTP responses or views.
  • Keep controllers clean and focused on business logic.
  • Guarantee a consistent error format across your API or site.

Use @RestControllerAdvice for APIs (JSON responses), and @ControllerAdvice for MVC (views/templates).


Basic Functionality & How It Works

When a controller throws an exception, Spring MVC searches for an @ExceptionHandler method that can handle its type. If such a method is found in an applicable @ControllerAdvice (or the controller itself), itโ€™s invoked to create the HTTP response (for REST) or render an error view (for MVC).

@RestControllerAdvice
public class GlobalRestExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
        ApiError body = ApiError.of(404, "RESOURCE_NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }
}

class ApiError {
    @Getter
    private int status;
    @Getter
    private String code;
    @Getter
    private String message;

    public static ApiError of(int status, String code, String message) {
        ApiError e = new ApiError();
        e.status = status; e.code = code; e.message = message;
        return e;
    }
}

@RestControllerAdvice vs @ControllerAdvice

  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody

    Best for RESTโ€”handlers return serialized bodies (JSON/XML).

  • @ControllerAdvice

Best for MVCโ€”handlers typically return a ModelAndView, a view name, or a redirect.

You can use both in the same project (one for REST controllers, one for MVC controllers).


Scoping & Organization Patterns

You can limit where advice applies to keep boundaries clean:

@RestControllerAdvice(basePackages = "in.stacknowledge.orders")
public class OrdersRestExceptionHandler { }

@ControllerAdvice(assignableTypes = {AdminController.class})
public class AdminMvcExceptionHandler { }

If you have multiple advices that could match the same exception, control precedence with @Order.


Handling Different Kinds of Exceptions (with code)

The following catalog covers the most common exception scenarios youโ€™ll face.

1) Validation Errors (Bean Validation)

Bodies (@RequestBody DTOs): MethodArgumentNotValidException

Params/Path (@Validated): ConstraintViolationException

@RestControllerAdvice
public class ValidationExceptionHandler extends ResponseEntityExceptionHandler {

    // For @Valid on @RequestBody DTOs
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers, HttpStatusCode status, WebRequest request) {

        Map<String, String> fieldErrors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                fe -> fe.getField(),
                fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
                (a, b) -> a
            ));

        ApiError body = ApiError.of(400, "VALIDATION_FAILED", "One or more fields are invalid.");
        // optionally: body.setDetails(fieldErrors);
        return ResponseEntity.badRequest().body(body);
    }

    // For @Validated on query/path parameters
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiError> handleConstraintViolation(ConstraintViolationException ex) {
        Map<String, String> violations = ex.getConstraintViolations().stream()
            .collect(Collectors.toMap(
                v -> v.getPropertyPath().toString(),
                v -> v.getMessage(),
                (a, b) -> a
            ));
        ApiError body = ApiError.of(400, "CONSTRAINT_VIOLATION", "Invalid request parameters.");
        // body.setDetails(violations);
        return ResponseEntity.badRequest().body(body);
    }
}

2) Type Mismatch & Binding Errors

  • MethodArgumentTypeMismatchException: wrong type for path/query param
  • BindException: binding errors in form submissions (MVC) or when binding non-body parameters
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiError> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
    String expected = ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown";
    String msg = "Parameter '%s' should be of type '%s'".formatted(ex.getName(), expected);
    return ResponseEntity.badRequest().body(ApiError.of(400, "TYPE_MISMATCH", msg));
}

3) Malformed/Unreadable Request Body

HttpMessageNotReadableException: malformed JSON, wrong data types in body

@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(
        HttpMessageNotReadableException ex,
        HttpHeaders headers, HttpStatusCode status, WebRequest request) {
    ApiError body = ApiError.of(400, "MALFORMED_JSON", "Invalid or unreadable JSON payload.");
    return ResponseEntity.badRequest().body(body);
}

4) Not Found

Custom ResourceNotFoundException or NoSuchElementException

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ApiError.of(404, "RESOURCE_NOT_FOUND", ex.getMessage()));
}

5) Business Rule Violations

Custom domain exceptions like BusinessException, InsufficientBalanceException โ†’ typically 422 Unprocessable Entity

@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ApiError> handleBusiness(InsufficientBalanceException ex) {
    ApiError body = ApiError.of(422, "INSUFFICIENT_BALANCE", ex.getMessage());
    // body.setDetails(Map.of("deficit", ex.getDeficit()));
    return ResponseEntity.status(422).body(body);
}

6) Security & Auth

  • AccessDeniedException โ†’ 403
  • AuthenticationException โ†’ typically handled by Spring Securityโ€™s AuthenticationEntryPoint (for REST). If not, map to 401
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiError> handleAccessDenied(AccessDeniedException ex) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(ApiError.of(403, "FORBIDDEN", "You do not have permission to access this resource."));
}

For REST APIs, prefer configuring an AuthenticationEntryPoint (401) and AccessDeniedHandler (403) in Spring Security; they run earlier than controller advice and ensure consistent security semantics.

7) Unsupported Media/Method

  • HttpMediaTypeNotSupportedException โ†’ 415
  • HttpRequestMethodNotSupportedException โ†’ 405

Override in ResponseEntityExceptionHandler or add explicit @ExceptionHandler methods.

8) 404 for Missing Routes

By default, Spring Bootโ€™s BasicErrorController handles 404. If you want your advice to catch โ€œno handler foundโ€, enable:

spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false

Then handle NoHandlerFoundException in your advice

9) Catch-All Fallback (Last Resort)

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGeneric(Exception ex) {
    // log the full stack trace on server side
    ApiError body = ApiError.of(500, "INTERNAL_ERROR", "Something went wrong. Please try again later.");
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}

Best Practices for REST APIs

1.ย  Consistent Error Contract

ย Define a uniform error body (custom ApiError or RFC 7807 ProblemDetail). Include:

  • status, code, message, optional details, and a traceId for support.

2.ย  Map Exceptions to Correct Status Codes

  • 400: validation, type mismatch, unreadable body
  • 401: unauthenticated (via AuthenticationEntryPoint)
  • 403: unauthorized (access denied)
  • 404: resource not found
  • 405/415: method/media type issues
  • 422: business rule violations
  • 429: rate-limited
  • 5xx: server/internal errors

3.ย  Do Not Leak Internals

Never return stack traces, SQL, or class names. Use a userโ€‘safe message, log the rest.

4.ย  Use Stable, Documented Error Codes

E.g., USER_NOT_FOUND, ORDER_STATE_INVALID. Keep them stable for clients.

5.ย  Field-Level Validation Details

Aggregate and send precise messages for invalid fields/params.

6.ย  Security Integration

Use AuthenticationEntryPoint and AccessDeniedHandler to produce JSON errors for security failures.

7.ย  Observability

Include traceId (from MDC/Tracing) in both logs and responses.

8.ย  Content Type

Always set Content-Type of error responses (e.g., application/json or application/problem+json for RFC 7807).

9.ย  Document Errors

In OpenAPI/Swagger, define response schemas and examples for error cases.

10. Test Negative Paths

With `MockMvc`/`WebTestClient`, verify status codes and error body structure.


Best Practices for MVC (Server-Rendered) Apps

1.ย  Return Error Views, Not JSON

Use @ControllerAdvice that returns ModelAndView/view names with friendly messages.

2.ย  Provide Status-Specific Pages

Place templates in templates/error/ (Thymeleaf): 404.html, 403.html, 500.html, etc.

3.ย  Customize Error Attributes

ย ย ย ย For /error, provide friendly attributes, but never expose stack traces to users.

4.ย  Form Validation

Use BindingResult with form submissions to show field-level messages inline.

5.ย  Consistent Layout

Reuse a common error layout/header/footer; include a support contact and a link back to home.

Example MVC advice:

@ControllerAdvice
public class GlobalMvcExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ModelAndView handleNotFound(ResourceNotFoundException ex) {
        ModelAndView mav = new ModelAndView("error/404");
        mav.addObject("message", ex.getMessage());
        return mav;
    }

    @ExceptionHandler(Exception.class)
    public ModelAndView handleGeneric(Exception ex) {
        // log ex
        ModelAndView mav = new ModelAndView("error/500");
        mav.addObject("message", "We hit a snag. Please try again later.");
        return mav;
    }
}

Custom Error Attributes

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest request, ErrorAttributeOptions options) {
        Map<String, Object> attrs = super.getErrorAttributes(
            request, options.excluding(ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.STACK_TRACE)
        );
        attrs.put("supportEmail", "contact@stacknowledge.in");
        return attrs;
    }
}

Problem Details (RFC 7807) in Spring 6/Boot 3+

Spring Boot 3 introduces first-class support for RFC 7807 via ProblemDetail. It standardizes error responses.

@RestControllerAdvice
public class ProblemDetailsHandler {

    @ExceptionHandler(BusinessException.class)
    public ProblemDetail handleBusiness(BusinessException ex) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY);
        pd.setTitle("Business rule violation");
        pd.setDetail(ex.getMessage());
        pd.setProperty("code", "BUSINESS_RULE_VIOLATION");
        return pd; // serialized as application/problem+json
    }
}

For REST, prefer ProblemDetail going forwardโ€”clients benefit from a standardized format.


Observability, Security & Hardening

Logging Levels

  • 4xx โ†’ WARN (client error)
  • 5xx โ†’ ERROR (server error)

Correlation/Trace IDs

Generate/propagate a traceId (e.g., via a filter and MDC) and return it in responses and logs.

PII/Sensitive Data

Redact or omit sensitive fields from error messages and logs.

Rate Limiting

For abusive patterns, respond with 429 Too Many Requests.

Security Errors

Keep messages generic to avoid information leaks (e.g., donโ€™t reveal which field failed in authentication)


Common Anti-Patterns

  • Returning raw exception messages to clients.
  • Catching Exception everywhere and swallowing it without logging.
  • Inconsistent error shapes across endpoints/services.
  • Treating all errors as 200 OK with a โ€œsuccess=falseโ€ body (breaks HTTP semantics).
  • Mixing REST and MVC responses in the same controller/advice unintentionally.