How do you handle an exception thrown by a WebFlux controller method, both locally and globally?
answer
- @ExceptionHandler = local; @ControllerAdvice = global
- @RestControllerAdvice adds @ResponseBody
- Handler can return Mono<ResponseEntity>
- Local wins over advice; most-specific type wins
- Only catches controller-method errors, not filter errors
basics
~10 sAdd a method annotated with @ExceptionHandler in the same controller to handle it locally. To handle exceptions from many controllers in one place, put @ExceptionHandler methods in a class annotated with @ControllerAdvice (or @RestControllerAdvice).
solid answer
~40 sIn WebFlux, @ExceptionHandler works much like in Spring MVC. A method annotated with @ExceptionHandler(SomeException.class) inside a @Controller/@RestController handles exceptions raised by that controller's request-mapping methods. To share handling across controllers, move those methods into a class annotated with @ControllerAdvice (or @RestControllerAdvice, which adds @ResponseBody so return values are serialized). Handler methods can return reactive types — Mono<ResponseEntity<...>>, Mono<ErrorBody>, etc. — and Spring resolves the return value exactly like a normal controller response. You can set the HTTP status with @ResponseStatus or by returning a ResponseEntity. Local controller handlers take precedence over @ControllerAdvice ones. This layer only catches exceptions that surface from controller invocation; errors from filters or outside the handler chain fall through to the global WebExceptionHandler chain.
code
java · 17 lines@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public Mono<ResponseEntity<ApiError>> handleNotFound(ResourceNotFoundException ex,
ServerWebExchange exchange) {
ApiError body = new ApiError("NOT_FOUND", ex.getMessage(),
exchange.getRequest().getPath().value());
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(body));
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // sets status; return value is the body
public ApiError handleBadRequest(IllegalArgumentException ex) {
return new ApiError("BAD_REQUEST", ex.getMessage(), null);
}
}go deeper
Know the two annotations and that local beats global.
Know handlers can return reactive types and that only controller-invocation errors are caught.
Explain the boundary vs the WebExceptionHandler chain and functional endpoints.
Discuss when to use annotation handlers vs a global ErrorWebExceptionHandler across mixed annotated/functional apps.
## What this covers WebFlux (Spring's reactive, non-blocking web stack running on Reactor `Mono`/`Flux`) offers the same annotation-based exception handling model as Spring MVC. ### @ExceptionHandler (controller-local) Inside a `@RestController` you can declare a method annotated with `@ExceptionHandler(MyException.class)`. When any `@RequestMapping`/`@GetMapping`/etc. method in that same controller throws (or returns a `Mono` that errors with) `MyException`, Spring invokes the handler method instead. The handler's return value is rendered exactly like a normal controller return value: - Return a `ResponseEntity<T>` (or `Mono<ResponseEntity<T>>`) to control status + body + headers. - Return a plain body object; set the status separately with `@ResponseStatus(HttpStatus.BAD_REQUEST)` on the handler method. - Return reactive types: `Mono<ErrorResponse>`, `Flux<...>` — these are supported because WebFlux resolves return values asynchronously. Handler methods can inject useful arguments: the exception itself, `ServerWebExchange`, `ServerHttpRequest`, `ServerHttpResponse`, `WebSession` (as `Mono`), etc. (Servlet types like `HttpServletRequest` are NOT available — this is reactive.) ### @ControllerAdvice / @RestControllerAdvice (global) Moving `@ExceptionHandler` methods into a `@ControllerAdvice`-annotated class makes them apply across all (or a filtered subset of) controllers. `@RestControllerAdvice` = `@ControllerAdvice` + `@ResponseBody`, so returned objects are serialized (JSON) rather than treated as view names. You can scope advice with `@ControllerAdvice(basePackages=...)`, `assignableTypes=...`, or `annotations=...`. ### Precedence 1. A matching `@ExceptionHandler` in the throwing controller wins over an equally-specific one in `@ControllerAdvice`. 2. Among candidates, Spring picks the most specific exception-type match (closest superclass), similar to catch-block resolution. ### Scope / boundary This annotation layer only handles exceptions that arise from **controller (annotated-handler) invocation** — it is implemented by `ExceptionHandlingWebHandler` delegating to `WebExceptionHandler`s, but the `@ExceptionHandler` resolution itself lives in the handler-adapter machinery. Exceptions thrown by a `WebFilter`, by the routing before a controller is selected, or in functional (`RouterFunction`) endpoints are NOT seen by `@ExceptionHandler`; those fall through to the global `WebExceptionHandler` chain (ending in Boot's `DefaultErrorWebExceptionHandler`). ### Gotchas - Returning `Mono.error(ex)` from a controller is equivalent to throwing — it still routes to `@ExceptionHandler`. - Don't block inside a handler (no `.block()`); return reactive types. - `@ExceptionHandler` catches errors on the controller method's returned publisher, but not errors that happen *while the response body is being written/streamed* after the status line is committed — at that point the response is already committed and cannot be changed.
- Does @ExceptionHandler work for functional (RouterFunction) endpoints?No. @ExceptionHandler is bound to annotated controllers. Functional endpoints handle errors with onErrorResume in the handler function or fall through to the global WebExceptionHandler chain.
- What's the difference between @ControllerAdvice and @RestControllerAdvice?@RestControllerAdvice is @ControllerAdvice plus @ResponseBody, so returned objects are serialized to the body (JSON) instead of being interpreted as a view/template name.
saying these in an interview costs you the question
- Claiming @ExceptionHandler can inject HttpServletRequest (that's the blocking Servlet stack, not WebFlux)
- Calling .block() inside a reactive exception handler
- Thinking @ExceptionHandler catches WebFilter or router-level errors