What is the WebExceptionHandler interface and how does the global exception-handling chain work in WebFlux?
answer
- Mono<Void> handle(exchange, throwable)
- ExceptionHandlingWebHandler wraps the chain
- Ordered beans; first to complete wins
- Catches filter + functional-endpoint errors too
- DefaultErrorWebExceptionHandler order -1
basics
~20 sWebExceptionHandler is a functional interface with one method: Mono<Void> handle(ServerWebExchange exchange, Throwable ex). Beans of this type form an ordered chain that catches exceptions not handled inside controllers. The first one to complete the exchange wins.
solid answer
~40 sWebExceptionHandler is WebFlux's low-level global error hook: `Mono<Void> handle(ServerWebExchange exchange, Throwable ex)`. The reactive `WebHttpHandlerBuilder` wraps the main `WebHandler` with an `ExceptionHandlingWebHandler` that holds an ordered list of `WebExceptionHandler` beans. When the request pipeline errors, each handler is offered the exchange in order; a handler either writes a response and returns a completing `Mono<Void>`, or re-signals the error (`Mono.error`) to pass it down the chain. Built-in handlers include `WebFluxResponseStatusExceptionHandler` (maps `ResponseStatusException` and `@ResponseStatus`-annotated exceptions to the right status). In a Spring Boot app, `DefaultErrorWebExceptionHandler` (an `ErrorWebExceptionHandler`, ordered at -1) sits at the front and renders a JSON/HTML error response for anything unhandled. Because it operates on the raw exchange, this layer also catches errors from filters and functional endpoints that `@ExceptionHandler` never sees.
code
java · 18 lines@Component
@Order(-2) // before Boot's DefaultErrorWebExceptionHandler (-1) if you want to pre-empt it
public class RateLimitExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (!(ex instanceof RateLimitExceededException)) {
return Mono.error(ex); // not mine -> pass down the chain
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Retry-After", "5");
byte[] bytes = "{\"error\":\"rate_limited\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(Mono.just(buffer));
}
}go deeper
Just know it exists as the global reactive error hook with a single handle method.
Explain the chain, ordering, and that it catches filter/functional errors.
Discuss committed-response limits and choosing this vs ErrorAttributes/annotation handlers.
Reason about ordering against Boot's -1 handler and owning global error rendering platform-wide.
## The interface ```java @FunctionalInterface public interface WebExceptionHandler { Mono<Void> handle(ServerWebExchange exchange, Throwable ex); } ``` It is the **lowest-level, most general** error hook in WebFlux. It receives the `ServerWebExchange` (request + response + attributes) and the `Throwable`, and returns `Mono<Void>` representing completion of response handling. ### How the chain is assembled When a WebFlux app boots, `WebHttpHandlerBuilder` builds the HTTP handler pipeline: `HttpWebHandlerAdapter` -> `ExceptionHandlingWebHandler` -> `FilteringWebHandler` (WebFilters) -> `DispatcherHandler` (routing to controllers/functional endpoints). `ExceptionHandlingWebHandler` wraps the delegate handler and holds an **ordered list** of `WebExceptionHandler` beans (ordered via `@Order`/`Ordered`/`@Priority`). If the delegate's `Mono` signals `onError`, the error is fed to the handlers **in order**: - A handler that can deal with the error writes to the response and returns a **completing** `Mono<Void>` (`Mono.empty()` after writing) — the chain stops. - A handler that can't deal with it returns `Mono.error(ex)` (typically by not matching), letting the next handler try. ### Built-in handlers - `WebFluxResponseStatusExceptionHandler` (core WebFlux): resolves `ResponseStatusException` and exceptions meta-annotated with `@ResponseStatus`, setting the response status. It leaves other exceptions to propagate. - Spring Boot adds `DefaultErrorWebExceptionHandler`, which implements `ErrorWebExceptionHandler` (a marker sub-interface of `WebExceptionHandler`). It is registered by `ErrorWebFluxAutoConfiguration` with order `-1` so it runs **before** the core handlers and can render a complete error response (JSON by default, or a Whitelabel/HTML page for browsers) for anything. ### Why it matters vs @ExceptionHandler `@ExceptionHandler`/`@ControllerAdvice` only see errors from **annotated controller invocation**. `WebExceptionHandler` operates on the raw exchange **outside** and **around** the `DispatcherHandler`, so it also catches: - errors thrown by `WebFilter`s, - errors in functional (`RouterFunction`) endpoints, - errors that occur before a handler is matched (e.g., 404 routing). ### Gotchas - Ordering matters: a lower order value = higher precedence. Boot's `DefaultErrorWebExceptionHandler` at `-1` intentionally precedes core handlers so Boot fully owns error rendering. - Once the response is **committed** (status/headers sent — e.g., mid-stream failure of a `Flux` body), a `WebExceptionHandler` can no longer change status; it can only complete/abort. - Returning `Mono.empty()` without writing a response leaves the client with an incomplete/empty response — always write something or re-signal the error. - This is a bean-based extension point: define a `@Bean` implementing `WebExceptionHandler` (with an order) to inject custom global handling — but in Boot apps most people customize `ErrorAttributes` or subclass `AbstractErrorWebExceptionHandler` instead.
- How is the ordering between multiple WebExceptionHandler beans decided?By Spring's ordering: @Order/Ordered/@Priority, lowest value first. ExceptionHandlingWebHandler consults them in that order; Boot's DefaultErrorWebExceptionHandler uses -1 to run near the front.
- Why can't a WebExceptionHandler always change the HTTP status?If the response is already committed (status line and headers flushed, e.g., a streaming Flux body that errors mid-write), the status cannot be changed anymore; the handler can only terminate the response.
saying these in an interview costs you the question
- Saying WebExceptionHandler only handles controller exceptions (it's broader)
- Returning Mono.empty() without writing a response
- Confusing higher order value with higher precedence (it's the opposite)