Compare onErrorReturn, onErrorResume, and onErrorMap. When would you choose each?
answer
- Return = constant + complete
- Resume = alternate Publisher (async fallback)
- Map = translate exception, still fails
- all have Class/Predicate overloads
- scope by exception type, don't hide NPEs
basics
~10 sonErrorReturn emits a fixed fallback value then completes. onErrorResume switches to another Mono/Flux (e.g., a cache or default stream). onErrorMap translates the exception into a different one but the stream still ends in error.
solid answer
~50 sAll three intercept an upstream error but do different things. `onErrorReturn(value)` — or `onErrorReturn(Predicate/Class, value)` — replaces the error with one static fallback value and completes; use it for a cheap constant default. `onErrorResume(throwable -> Publisher)` swaps in an entire alternate publisher, so you can call a cache, a secondary service, or return `Mono.empty()`; use it when the fallback itself is dynamic or asynchronous. `onErrorMap(throwable -> throwable)` does *not* recover — it translates the exception (e.g., wrap a low-level `SQLException` into a domain `RepositoryException`) and lets it keep propagating as a terminal error; use it to normalize exception types at a boundary. Rule of thumb: Return for a constant, Resume for a fallback flow, Map when you still want to fail but with a cleaner exception. All accept a predicate/class overload so you only catch specific exception types.
code
java · 11 linesMono<User> loadUser(String id) {
return userClient.fetch(id) // may fail
// 1) translate infra -> domain, still an error
.onErrorMap(WebClientResponseException.class,
e -> new UserServiceUnavailable(id, e))
// 2) dynamic async fallback to cache on that domain error
.onErrorResume(UserServiceUnavailable.class,
e -> cache.lookup(id))
// 3) last-resort constant default, then complete
.onErrorReturn(NotFoundException.class, User.anonymous());
}go deeper
Knows Return gives a default and Resume gives a fallback stream.
Must correctly separate the three and know onErrorMap does not recover.
Uses Class/Predicate overloads deliberately and places onErrorMap at boundaries.
Designs exception-translation policy across modules and warns against error-masking anti-patterns.
## The three interception operators Each is available on both `Mono` and `Flux` and each fires only on an **upstream** `onError` signal (never on normal completion). ### 1. `onErrorReturn` Signatures include: - `onErrorReturn(T fallbackValue)` - `onErrorReturn(Class<? extends Throwable> type, T fallbackValue)` - `onErrorReturn(Predicate<? super Throwable> predicate, T fallbackValue)` On a matching error it **emits one fallback value and then completes**. It's the cheapest recovery when a single constant is acceptable (e.g., an empty list, `false`, a default DTO). It cannot do async work — the value must already exist. ### 2. `onErrorResume` Signature: `onErrorResume(Function<? super Throwable, ? extends Publisher<? extends T>>)` plus `Class`/`Predicate` overloads. On a matching error it **subscribes to the publisher you return**, effectively continuing the stream from that alternate source. This is the most powerful/general recovery: you can - return `Mono.just(default)` — equivalent to onErrorReturn, - return `Mono.empty()` — swallow the error and complete empty, - call a fallback service / cache: `err -> cache.lookup(id)`, - re-raise selectively: `err -> Mono.error(new DomainException(err))`. Because the function receives the `Throwable`, you can branch on the failure. Use it whenever the fallback requires I/O or logic. ### 3. `onErrorMap` Signature: `onErrorMap(Function<? super Throwable, ? extends Throwable>)` plus `Class`/`Predicate` overloads. It **does not recover**. It transforms the throwable and re-emits `onError` with the new exception, keeping the terminal-error semantics. Classic use: at a module/adapter boundary, translate infrastructure exceptions into domain exceptions so callers don't depend on `SQLException`, `WebClientResponseException`, etc. Without it, people often write `onErrorResume(e -> Mono.error(map(e)))`, which works but `onErrorMap` expresses the intent more clearly. ## Choosing | Need | Operator | |------|----------| | Constant default, then succeed | `onErrorReturn` | | Dynamic/async fallback, or swallow to empty | `onErrorResume` | | Still fail, but with a cleaner exception type | `onErrorMap` | ## Gotchas - **Scope by type.** Prefer the `Class`/`Predicate` overloads so you don't accidentally recover from unrelated errors (e.g., only catch `TimeoutException`, not `NullPointerException` bugs). - **Placement.** These catch only errors from operators upstream of them. Put `onErrorMap` at the boundary where you know the exception meaning. - **onErrorReturn vs empty.** `onErrorReturn` emits a value; if you want "complete with nothing", use `onErrorResume(e -> Mono.empty())`. - **Don't hide bugs.** A broad `onErrorResume(e -> Mono.empty())` can mask programming errors (NPEs). Filter by exception type. - **onErrorMap keeps it terminal.** After onErrorMap the subscriber still receives onError — it is *not* a recovery.
- How do you make onErrorResume catch only a specific exception type?Use the overload onErrorResume(Class<? extends Throwable>, fn) or onErrorResume(Predicate, fn). Other exception types then pass through untouched, so real bugs aren't masked.
- onErrorReturn vs onErrorResume(e -> Mono.just(x)) — any difference?Functionally similar for a constant, but onErrorReturn is eager/static and clearer for a fixed value; onErrorResume lets the fallback be computed lazily or asynchronously per-error.
saying these in an interview costs you the question
- Thinking onErrorMap recovers the stream (it still ends in error)
- Using a broad onErrorResume that swallows NPEs and hides bugs
- Believing onErrorReturn can perform an async/IO fallback
- Not scoping handlers by exception type