What does onErrorContinue do, why is it considered controversial, and how does it differ from onErrorResume inside a flatMap?
answer
- drops bad element, stream continues (not terminal)
- installs handler in Reactor Context — operators must opt in
- action at a distance / non-local surprise
- prefer flatMap(x -> process(x).onErrorResume(e -> empty))
- many teams ban it
basics
~20 sonErrorContinue drops the element that caused an error and keeps the stream going instead of terminating. It's controversial because it breaks the 'error is terminal' model and only works with operators that explicitly support it, so behavior is surprising. Prefer onErrorResume inside flatMap.
solid answer
~50 s`onErrorContinue(BiConsumer<Throwable, Object>)` lets a sequence **survive** an error: the offending element is dropped, your callback receives the throwable and the bad value, and upstream keeps emitting instead of terminating. It works by placing a recovery handler in the Reactor `Context`; **only operators that opt in** (like `map`, `flatMap`, `filter`) check that context and divert their errors to it. This is why it's controversial: it violates the normal 'onError is terminal' contract, it acts non-locally (an operator many lines up changes behavior because of a downstream `onErrorContinue`), and it silently does nothing for operators that don't support it — leading to confusing bugs. The idiomatic, explicit alternative is to handle errors **inside** the inner publisher: `flatMap(x -> process(x).onErrorResume(e -> Mono.empty()))`. That contains the error to one element locally, is easy to reason about, and doesn't depend on operator cooperation. Many teams ban `onErrorContinue` for exactly these reasons.
code
java · 15 lines// onErrorContinue: survives bad elements (controversial)
Flux.just("1", "2", "x", "4")
.map(Integer::parseInt) // 'x' throws
.onErrorContinue((err, value) ->
log.warn("dropped {}: {}", value, err.toString()))
.subscribe(System.out::println); // prints 1, 2, 4 (x skipped)
// Preferred: local, explicit handling inside flatMap
Flux.just("1", "2", "x", "4")
.flatMap(s -> Mono.fromCallable(() -> Integer.parseInt(s))
.onErrorResume(e -> {
log.warn("skip {}", s, e);
return Mono.empty(); // drop this element only
}))
.subscribe(System.out::println); // 1, 2, 4 — no Context magicgo deeper
Likely unaware of this operator; fine to not know it.
Knows it skips failing elements and keeps the stream alive.
Must explain the Context/opt-in mechanism, the non-local caveats, and the flatMap alternative.
Sets team policy (often banning it) and reasons about predictability, maintainability, and error-model integrity.
## What onErrorContinue does `onErrorContinue(BiConsumer<Throwable, Object> errorConsumer)` (with `Class`/`Predicate` overloads) changes the semantics of **upstream** operators so that, instead of an error being terminal, the **element that triggered the error is dropped** and the sequence **continues** with the next element. Your `BiConsumer` receives `(theThrowable, theOffendingValue)` for logging/metrics. Example: mapping over 100 records, a couple of which throw — with `onErrorContinue` you process the other 98 and skip the 2 bad ones, rather than aborting the whole Flux on the first bad record. ## How it works — the Context trick (the source of the controversy) `onErrorContinue` does **not** wrap upstream like a normal operator. It installs the error handler into the subscription's **Reactor `Context`** (`OnNextFailureStrategy`). Then, individual operators must **opt in**: their implementation checks the Context and, on an error, routes it to the continue-handler and **drops the value** instead of signaling `onError`. Operators that support it include `map`, `flatMap`, `concatMap`, `filter`, `handle`, and several others. Operators that **don't** support it ignore the strategy entirely and error normally. ### Why that's problematic 1. **Breaks the terminal-error contract.** Reactive Streams says onError is terminal; `onErrorContinue` makes it not so, which surprises readers. 2. **Action at a distance / non-local.** A single `onErrorContinue` at the bottom silently alters how operators far upstream behave. Refactoring or reordering can change semantics invisibly. 3. **Inconsistent operator support.** It only affects operators that explicitly cooperate. Put it above one that doesn't and it does nothing — an easy, silent bug. 4. **Leaks across boundaries.** Because it rides in the Context, it can affect inner publishers inside `flatMap` in ways that are hard to predict, sometimes swallowing errors you meant to keep. ## The idiomatic alternative: handle inside flatMap Handle the error **locally on the inner publisher**, where the failing element is in scope: ```java flux.flatMap(item -> process(item) .onErrorResume(e -> { log.warn("skip {}", item, e); return Mono.empty(); })) ``` Here the error is contained to that one `item`: `onErrorResume(... Mono.empty())` drops it and the outer Flux continues, because a completed-empty inner publisher just contributes nothing. This is **explicit, local, and independent of operator cooperation** — you can read exactly what happens without knowing about a distant operator or the Context mechanism. ## When (if ever) to use onErrorContinue - Rarely. Maybe a quick batch job where you truly want best-effort processing and don't care about the failure model. Even then, the flatMap+onErrorResume pattern is usually clearer. - The Reactor docs themselves flag it as an advanced operator with caveats and recommend the inner-handling approach for predictability. ## Gotchas - Combining `onErrorContinue` with operators that fuse or don't support it yields surprising results; don't assume it applies everywhere. - `doOnError` still fires for observed errors but doesn't stop continuation. - It only affects operators **upstream** of it, like other error operators. - Do not use it to mask real bugs (NPEs) across a large pipeline; scope by exception type if you must use it.
- Why can onErrorContinue silently do nothing?It relies on upstream operators opting into the Context-based OnNextFailureStrategy. If the operator directly above doesn't support it, the error terminates normally and the continue-handler is never invoked.
- How does flatMap + onErrorResume(Mono.empty()) achieve per-element skipping without onErrorContinue?The inner publisher for the failing element recovers to an empty Mono, so it contributes no value and completes; the outer Flux keeps merging the remaining inner publishers, effectively dropping just that element locally.
saying these in an interview costs you the question
- Thinking onErrorContinue works uniformly with every operator
- Not knowing it uses the Reactor Context and requires operator opt-in
- Believing it doesn't break the terminal-error contract
- Preferring it over local flatMap+onErrorResume for readable code