Across @ExceptionHandler, WebExceptionHandler, and Boot's error handler, how do you decide which layer owns error handling — and in what order do they see an exception?
answer
- Tier 1 annotations inside DispatcherHandler; Tier 2 global chain around it
- Local -> advice -> WebExceptionHandler chain (order -1 Boot)
- Functional/filter/routing errors skip @ExceptionHandler
- Advice for typed API bodies; ErrorAttributes for uniform fallback
- Committed response = can't change status
basics
~20 sA controller error is offered first to @ExceptionHandler (local then @ControllerAdvice). If unhandled — or if it came from a filter/functional endpoint — it propagates to the ordered WebExceptionHandler chain, ending in Boot's DefaultErrorWebExceptionHandler (order -1) which renders from ErrorAttributes.
solid answer
~50 sThink of two tiers. Tier 1 is the annotation model inside the `DispatcherHandler`: an exception from an annotated controller method is resolved against `@ExceptionHandler` methods — local to the controller first, then matching `@ControllerAdvice` — picking the most specific exception type. Tier 2 is the global `WebExceptionHandler` chain around the whole pipeline (`ExceptionHandlingWebHandler`), reached when Tier 1 doesn't apply: functional endpoints, `WebFilter` errors, routing errors, or exceptions no `@ExceptionHandler` matches. That chain is ordered; Boot's `DefaultErrorWebExceptionHandler` sits at -1 and renders a content-negotiated body from `ErrorAttributes`. Choose the layer by scope and shape of client: use `@ControllerAdvice` for domain-to-HTTP mapping of annotated APIs with rich, typed bodies; customize `ErrorAttributes` (or subclass `AbstractErrorWebExceptionHandler`) for a uniform fallback contract spanning both annotated and functional endpoints; add a bespoke `WebExceptionHandler` only for cross-cutting concerns that must pre-empt Boot.
code
java · 23 lines// Layered strategy:
// 1) Typed domain->HTTP mapping for annotated controllers.
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(BusinessException.class)
Mono<ResponseEntity<ApiError>> handle(BusinessException ex, ServerWebExchange ex2) {
return Mono.just(ResponseEntity.status(ex.status())
.body(new ApiError(ex.code(), ex.getMessage())));
}
}
// 2) Uniform FALLBACK schema covering functional endpoints + filter errors too.
@Component
class GlobalErrorAttributes extends DefaultErrorAttributes {
@Override public Map<String,Object> getErrorAttributes(ServerRequest req,
ErrorAttributeOptions opts) {
var m = super.getErrorAttributes(req, opts);
m.putIfAbsent("errorCode", "INTERNAL");
m.put("traceId", req.exchange().getRequest().getId());
m.remove("trace");
return m;
}
}go deeper
Know controller handlers run first, then a global fallback exists.
Order the layers and know functional/filter errors skip @ExceptionHandler.
Choose the right layer per concern and unify contracts across endpoint styles.
Design an org-wide error architecture: typed advice + uniform fallback, ordering, streaming/committed limits, and info-leak governance.
## The two tiers and the ordering of who sees an exception ### Tier 1 — annotation handling (inside DispatcherHandler) When an **annotated controller** method throws or returns an erroring `Mono`/`Flux`, the WebFlux handler-adapter machinery tries to resolve an `@ExceptionHandler`: 1. In the **same controller** first (local wins). 2. Then in `@ControllerAdvice`/`@RestControllerAdvice` beans (scoped by basePackages/assignableTypes/annotations). 3. Selection is by **most specific exception type** (nearest superclass match). If a handler matches, it produces the response and the exception is considered handled — the global chain never sees it. ### Tier 2 — the WebExceptionHandler chain (around everything) `WebHttpHandlerBuilder` wraps the pipeline with `ExceptionHandlingWebHandler`, holding an **ordered** list of `WebExceptionHandler` beans. An exception reaches this chain when: - it comes from a **functional endpoint** (`RouterFunction`) — no `@ExceptionHandler` applies; - it is thrown by a **`WebFilter`** or during **routing** (e.g., no handler found -> 404); - it escaped an annotated controller with **no matching** `@ExceptionHandler`. Handlers are consulted by `@Order` (lowest value first). Core WebFlux contributes `WebFluxResponseStatusExceptionHandler` (maps `ResponseStatusException`/`@ResponseStatus`). Spring Boot contributes `DefaultErrorWebExceptionHandler` at **order -1**, so it fronts the chain and renders JSON/HTML from `ErrorAttributes` — the universal fallback. ### The effective order an exception is offered handlers 1. Controller-local `@ExceptionHandler`. 2. `@ControllerAdvice` `@ExceptionHandler`. 3. (If it escaped or originated outside controllers) the `WebExceptionHandler` chain by order — Boot's `DefaultErrorWebExceptionHandler` (-1) typically first, then core handlers. ## Decision guide — which layer to own it - **`@ExceptionHandler` in `@RestControllerAdvice`**: best for translating **domain exceptions to HTTP** for annotated APIs, with typed, validated bodies, per-endpoint nuance, access to controller-style argument injection, and reactive return types. This is where most API error contracts live. - **Custom `ErrorAttributes` (extend `DefaultErrorAttributes`)**: best when you want a **single, uniform fallback body schema** (errorCode, traceId, path...) that must cover **both** annotated and functional endpoints and filter errors — because it feeds the global handler that catches everything. Low-risk, minimal code. - **Subclass `AbstractErrorWebExceptionHandler` / override `getRoutingFunction`**: when you need full control of routing/rendering (e.g., different media handling, problem+json, per-path responses) beyond field shaping. - **Bespoke `WebExceptionHandler` bean (order < -1)**: reserve for **cross-cutting infra concerns** that must pre-empt Boot entirely (rate-limit envelope, security scrubbing, tenant-specific error routing). Powerful but low-level; you own committed-response and media concerns. ## Edge cases / gotchas - **Committed responses**: if a streaming `Flux` body errors after the status/headers are flushed, no layer can change the status; only termination is possible. Design streaming endpoints to validate before writing. - **Consistency trap**: putting your entire error contract only in `@ControllerAdvice` leaves functional endpoints and filter errors falling back to Boot's default shape — mismatched bodies. Unify at the `ErrorAttributes`/handler layer if you have mixed endpoint styles. - **`ResponseStatusException`** is often the simplest tool: throw it and let `WebFluxResponseStatusExceptionHandler`/Boot set the status without any custom handler. - **Ordering pitfalls**: remember lower `@Order` = higher precedence; to run before Boot's default, use an order less than -1. - **Security**: whichever layer renders, gate stacktrace/exception-class exposure via `server.error.include-*` (they feed `ErrorAttributeOptions`). - **`@ExceptionHandler` cannot see** errors from the reactive plumbing outside controller invocation — don't rely on it for global guarantees.
- In a mixed app with annotated controllers and RouterFunction endpoints, where should the canonical error schema live and why?At the ErrorAttributes (or AbstractErrorWebExceptionHandler) layer, because that global path catches both endpoint styles plus filter/routing errors; @ControllerAdvice alone would leave functional/filter errors on Boot's default shape.
- How would you make a custom WebExceptionHandler run before Boot's DefaultErrorWebExceptionHandler?Register it as a bean with an @Order value less than -1 (Boot's default order), so ExceptionHandlingWebHandler offers it the exception first.
saying these in an interview costs you the question
- Assuming @ControllerAdvice covers functional-endpoint and filter errors
- Believing higher @Order value means higher precedence
- Thinking you can rewrite the status after the response body has started streaming