How does Spring resolve which @ExceptionHandler runs, and how do you control ordering across multiple @ControllerAdvice beans?
answer
- ExceptionHandlerExceptionResolver
- local controller handler beats global advice
- advices consulted in @Order; lowest value first
- most specific type wins; walks cause chain
- catch-all Exception at lowest precedence
basics
~20 sFor a thrown exception, Spring first looks for a matching @ExceptionHandler in the controller itself, then in the @ControllerAdvice beans, choosing the closest exception-type match. When several advices could match, their order (via @Order or Ordered) decides which is consulted first.
solid answer
~50 sThe ExceptionHandlerExceptionResolver drives resolution. On an exception, it first searches @ExceptionHandler methods local to the throwing controller; a local handler always beats a global one. If none matches locally, it consults the @ControllerAdvice beans. Advice beans are consulted in Ordered/@Order sequence (lowest value = highest precedence, unordered advices go last), and the first advice that has a matching handler wins — so ordering matters when two advices could both handle the same exception. Within whichever handler set is chosen, Spring picks the most specific exception match by walking the exception's type hierarchy (and its cause chain). Practical rules: keep a single catch-all @ExceptionHandler(Exception.class) in the lowest-precedence advice as a safety net; put specific/domain advices at higher precedence; avoid two unscoped advices handling the same type without an explicit @Order, or resolution becomes order-dependent and surprising.
code
java · 21 lines// Specialized advice — consulted first
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(assignableTypes = PaymentApi.class)
class PaymentExceptionHandler {
@ExceptionHandler(PaymentDeclinedException.class)
ProblemDetail declined(PaymentDeclinedException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.PAYMENT_REQUIRED, ex.getMessage());
}
}
// Generic fallback — lowest precedence, catches everything else
@Order(Ordered.LOWEST_PRECEDENCE)
@RestControllerAdvice
class GlobalFallbackHandler {
@ExceptionHandler(Exception.class)
ProblemDetail unexpected(Exception ex) {
// log ex with correlation id...
return ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error");
}
}go deeper
Know local controller handlers win and the most specific exception type is chosen.
Explain @Order/Ordered controls precedence among multiple advices.
Design a layered specialized-plus-fallback advice setup and understand cause-chain matching.
Reason about deterministic resolution across modules, shadowing pitfalls, and the resolver's scope limits.
## The resolver Exception handling for `@Controller`s is performed by **`ExceptionHandlerExceptionResolver`** (one of the `HandlerExceptionResolver`s registered by Spring MVC). When a handler method throws and the exception escapes, this resolver tries to find an `@ExceptionHandler` to produce the response. ## Step 1 — local before global It **first** looks for a matching `@ExceptionHandler` **declared in the controller class** that handled the request (cached per controller as `ExceptionHandlerMethodResolver`). A **local handler always takes precedence** over any global `@ControllerAdvice` for the same exception. Only if the controller has no matching local handler does it move on. ## Step 2 — advice beans in order It then iterates the detected `@ControllerAdvice` beans. Their consultation order is determined by Spring's ordering: implement **`Ordered`** or annotate with **`@Order`** — **lower value = higher precedence**. Advices with no explicit order are treated as lowest precedence (`Ordered.LOWEST_PRECEDENCE`) and consulted last, in an effectively unspecified relative order. The resolver uses the **first advice** (in that order) that contains a matching handler for the exception. So if two advices both handle `RuntimeException`, the higher-precedence one wins — ordering is the tie-breaker. (Scoping via `assignableTypes`/`annotations`/`basePackages` first filters which advices even apply to the current controller; ordering only matters among those that apply.) ## Step 3 — most specific exception match Within the chosen handler set (local set, or a given advice), Spring selects the **most specific** `@ExceptionHandler` by depth in the exception's type hierarchy. For a thrown `IllegalStateException`, a handler for `IllegalStateException` beats one for `RuntimeException`, which beats one for `Exception`. Spring also inspects the **cause chain**: if no handler matches the top exception, it can match a handler for the exception's `getCause()`. Ambiguous equally-specific matches throw an `IllegalStateException` at resolution time. ## Designing a robust hierarchy - One **global catch-all** `@ExceptionHandler(Exception.class)` (or `Throwable`) in a **lowest-precedence** advice → guarantees no unhandled exception leaks the container's default error page; log + return a generic 500 ProblemDetail. - **Specific/domain** advices (or handlers) at **higher precedence** for known exceptions with tailored status/body. - Prefer **scoping** advices to disjoint controller groups over relying on `@Order`, so behavior isn't order-sensitive. ## Gotchas - A local `@ExceptionHandler` silently **shadows** a global one — surprising when debugging why the global handler 'didn't run'. - Two unordered advices handling the same exception → **non-deterministic**-feeling precedence; always add `@Order` when overlaps are intentional. - The resolver only covers exceptions from **controller dispatch**; async, filter, and `ErrorController`/`error` dispatch paths differ. - A catch-all at **high** precedence accidentally swallows exceptions you meant a specific advice to handle — put the catch-all **last**. - Ordering also affects `@ModelAttribute`/`@InitBinder` contribution order across advices. ## When to use Most apps: a single advice, no ordering needed. Reach for explicit `@Order` when you deliberately layer a generic fallback advice beneath specialized ones, or run multiple advices that could overlap.
- You have a global @ExceptionHandler(MyException.class), but for one controller it isn't invoked. Likely cause?That controller (or a higher-precedence in-scope advice) declares a local/more-specific @ExceptionHandler that matches the exception and shadows the global one. Local handlers always win over global advice.
- Why should a catch-all @ExceptionHandler(Exception.class) advice have the LOWEST precedence?So it is consulted last. At high precedence it would match first and swallow exceptions that specialized advices were meant to handle with tailored status/body.
- If no handler matches the thrown exception but one matches its cause, what happens?Spring inspects the cause chain and can invoke the handler matching getCause(), so wrapping exceptions still get handled if a handler exists for the underlying cause type.
saying these in an interview costs you the question
- Thinking global advice runs before a controller's own @ExceptionHandler
- Assuming order among unannotated advices is deterministic
- Placing a catch-all Exception handler at highest precedence
- Believing only the top-level exception type is matched, ignoring the cause chain
- Expecting @ControllerAdvice to catch filter-chain or async-dispatch exceptions