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
@RestControllerAdvicefor APIs (JSON responses), and@ControllerAdvicefor 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 paramBindException: 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โ 403AuthenticationExceptionโ typically handled by Spring SecurityโsAuthenticationEntryPoint(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) andAccessDeniedHandler(403) in Spring Security; they run earlier than controller advice and ensure consistent security semantics.
7) Unsupported Media/Method
HttpMediaTypeNotSupportedExceptionโ 415HttpRequestMethodNotSupportedExceptionโ 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, optionaldetails, and atraceIdfor 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
ProblemDetailgoing 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
Exceptioneverywhere 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.



Post Comment