How does WebClient handle non-2xx HTTP responses by default, and how do you customize that with onStatus?
answer
- retrieve() = auto 4xx/5xx error
- WebClientResponseException + .NotFound/.BadRequest subclasses
- onStatus(predicate, resp -> Mono<Throwable>)
- exchangeToMono = no auto error, you release body
- handle with onErrorResume, not try/catch
basics
~10 sBy default, when you call retrieve(), WebClient turns 4xx/5xx responses into a WebClientResponseException error on the reactive stream. onStatus(predicate, handler) lets you intercept a status range and map it to your own exception.
solid answer
~40 sWith retrieve(), WebClient automatically signals an error for any 4xx or 5xx response — a WebClientResponseException (with subclasses like NotFound, BadRequest) carrying the status code, headers and raw body. That error flows through the reactive pipeline, so you handle it with onErrorResume/onErrorMap, not try/catch. onStatus(HttpStatusCode::isError-style predicate, response -> Mono<Throwable>) lets you override this per status range: inspect the ClientResponse, read the body, and return a Mono of a domain-specific exception. If you use exchangeToMono() instead of retrieve(), there is NO automatic error signal — you inspect statusCode() yourself and must consume the body to avoid leaks. onStatus only applies to the retrieve() path.
code
java · 14 linesMono<Account> account = webClient.get()
.uri("/accounts/{id}", id)
.retrieve()
// map 404 to a domain exception
.onStatus(status -> status.value() == 404,
response -> Mono.error(new AccountNotFoundException(id)))
// map any remaining 5xx to a custom exception, decoding the error body
.onStatus(HttpStatusCode::is5xxServerError,
response -> response.bodyToMono(ApiError.class)
.defaultIfEmpty(new ApiError("unknown"))
.map(err -> new UpstreamServerException(err.message())))
.bodyToMono(Account.class)
// reactive error handling, NOT try/catch
.onErrorResume(AccountNotFoundException.class, ex -> Mono.empty());go deeper
Know that retrieve() turns 4xx/5xx into WebClientResponseException and onStatus lets you remap a status range.
Know the subclasses (.NotFound etc.), that handlers evaluate in order first-match, and that errors are reactive signals handled with onErrorResume/onErrorMap.
Contrast retrieve() vs exchangeToMono(), body-release responsibilities, and decoding the error body inside onStatus.
Design a consistent upstream-error taxonomy (mapping status ranges to domain exceptions) shared across clients, plus body-size limits and observability on failures.
**WebClient** is Spring WebFlux's non-blocking, reactive HTTP client (the reactive successor to RestTemplate). A call returns a **Mono** (0..1 values) or **Flux** (0..N values) — reactive publishers that emit either data or an error signal. **Two ways to get the response:** - `retrieve()` — the common, concise path. It has *built-in* error handling: any response with a **4xx or 5xx** status is automatically converted into an error signal on the stream. - `exchangeToMono()` / `exchangeToFlux()` — full control. There is **no** automatic error; you check `response.statusCode()` yourself and you are responsible for consuming/releasing the body (e.g. `releaseBody()`), or you leak the connection. **The default exception:** on the `retrieve()` path a non-2xx status raises **`WebClientResponseException`**. It extends `WebClientException` (a `RuntimeException`). It exposes `getStatusCode()`, `getStatusText()`, `getHeaders()`, `getResponseBodyAsString()` / `getResponseBodyAsByteArray()`. Spring provides **status-specific subclasses**: `WebClientResponseException.NotFound` (404), `.BadRequest` (400), `.Unauthorized` (401), `.Forbidden` (403), `.InternalServerError` (500), etc. You can `catch`/`onErrorResume` on the specific subclass. **Customizing with `onStatus`:** the signature is `onStatus(Predicate<HttpStatusCode> statusPredicate, Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction)`. When the predicate matches the response status, your function runs, receives the `ClientResponse` (from which you can decode the error body), and returns a **`Mono<Throwable>`** — the exception that will be signalled instead of the default. You can register multiple `onStatus` handlers; they are evaluated in order and the **first matching** one wins. A status not matched by any handler falls back to the default 4xx/5xx behavior. **Important gotcha:** inside the `onStatus` exception function you should consume the body (e.g. `response.bodyToMono(ErrorBody.class)`) — if you return an exception without reading the body, the response body may not be released. Also: `onStatus` handlers only fire on the `retrieve()` path; they are ignored if you use `exchangeToMono()`. **Where errors go:** because the error is a *reactive* signal, `try/catch` around the WebClient call does not catch it — the exception surfaces when the pipeline is subscribed. Handle it with operators: `onErrorMap` (translate one exception to another), `onErrorResume` (provide a fallback Mono/Flux), or `onErrorReturn` (a static fallback value). **When to use what:** use `retrieve()` + `onStatus` for standard REST calls where you want status→exception mapping. Use `exchangeToMono()` when you must decode the body differently per status, stream the body, or handle a status *without* treating it as an error (e.g. a 404 that means 'return empty').
- Why can't you wrap a WebClient call in try/catch to handle a 500?The 500 becomes an error *signal* on the reactive stream, emitted asynchronously at subscription time — not a thrown exception at call-assembly time. You handle it with reactive operators like onErrorResume/onErrorMap, which run when the signal propagates.
- What's the difference between retrieve() and exchangeToMono() for error handling?retrieve() auto-converts 4xx/5xx into WebClientResponseException and supports onStatus. exchangeToMono() gives raw ClientResponse with no auto error — you inspect statusCode() and must consume/release the body yourself, or you leak the connection.
saying these in an interview costs you the question
- Thinking WebClient returns the response object for a 500 so you check a status field like RestTemplate — retrieve() signals an error instead.
- Trying to catch WebClientResponseException with try/catch around the call.
- Believing onStatus works with exchangeToMono() (it doesn't).
- Returning an exception from onStatus without consuming the body, leaking the connection.