skip to content

How do you handle an exception thrown by a WebFlux controller method, both locally and globally?

level: juniorimportance: must knowfreq 70%

answer

  1. @ExceptionHandler = local; @ControllerAdvice = global
  2. @RestControllerAdvice adds @ResponseBody
  3. Handler can return Mono<ResponseEntity>
  4. Local wins over advice; most-specific type wins
  5. Only catches controller-method errors, not filter errors

basics

~10 s

Add 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 s

In 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
java
@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

for a junior

Know the two annotations and that local beats global.

for a middle

Know handlers can return reactive types and that only controller-invocation errors are caught.

for a senior

Explain the boundary vs the WebExceptionHandler chain and functional endpoints.

for a principal

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

context