skip to content

Compare onErrorReturn, onErrorResume, and onErrorMap. When would you choose each?

level: middleimportance: must knowfreq 75%

answer

  1. Return = constant + complete
  2. Resume = alternate Publisher (async fallback)
  3. Map = translate exception, still fails
  4. all have Class/Predicate overloads
  5. scope by exception type, don't hide NPEs

basics

~10 s

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

All 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 lines
java
Mono<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

for a junior

Knows Return gives a default and Resume gives a fallback stream.

for a middle

Must correctly separate the three and know onErrorMap does not recover.

for a senior

Uses Class/Predicate overloads deliberately and places onErrorMap at boundaries.

for a principal

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

context