onErrorDropped false positive
rozza opened this issue · comments
I have a use case where one Flux
essentially chains async calls and flattens them.
I noticed even though I have doOnError
handling I get a onErrorDropped
error logged. However, when I add the same error handler via subscribe
I no longer get the onErrorDropped
logged.
Expected Behavior
I'd expect both approaches to be the same.
Actual Behavior
Handling the error via doOnError
results in a false positive onErrorDropped
.
Steps to Reproduce
@Test
void reproCase() {
AtomicReference<Throwable> throwableAtomicReference = new AtomicReference<>();
Hooks.onErrorDropped(throwableAtomicReference::set);
Hooks.onOperatorDebug();
try {
Flux.<Integer>create(sink -> {
AtomicInteger counter = new AtomicInteger(0);
while (counter.get() < 5) {
Mono.create((MonoSink<Integer> ms) -> {
if (counter.get() < 4) {
ms.success(counter.incrementAndGet());
} else {
counter.incrementAndGet();
ms.error(new RuntimeException());
}
})
.doOnNext(sink::next)
.doOnError(sink::error)
.subscribe();
}
}).collectList().block();
} catch (Exception e) {
// ignore
}
Throwable droppedError = throwableAtomicReference.get();
if (droppedError != null) {
throw new AssertionError("`onError` called with no handler.", droppedError);
}
}
Throws:
Exception in thread "main" java.lang.AssertionError: `onError` called with no handler.
at reactivestreams.tour.DroppedOnError.main(DroppedOnError.java:78)
Caused by: reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.RuntimeException
Caused by: java.lang.RuntimeException
at reactivestreams.tour.DroppedOnError.lambda$main$0(DroppedOnError.java:63)
Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
Assembly trace from producer [reactor.core.publisher.MonoCreate] :
reactor.core.publisher.Mono.create(Mono.java:202)
reactivestreams.tour.DroppedOnError.lambda$main$1(DroppedOnError.java:58)
Error has been observed at the following site(s):
*_______Mono.create ⇢ at reactivestreams.tour.DroppedOnError.lambda$main$1(DroppedOnError.java:58)
|_ Mono.doOnNext ⇢ at reactivestreams.tour.DroppedOnError.lambda$main$1(DroppedOnError.java:66)
|_ Mono.doOnError ⇢ at reactivestreams.tour.DroppedOnError.lambda$main$1(DroppedOnError.java:67)
*_______Flux.create ⇢ at reactivestreams.tour.DroppedOnError.main(DroppedOnError.java:55)
|_ Flux.collectList ⇢ at reactivestreams.tour.DroppedOnError.main(DroppedOnError.java:71)
Possible work around
Use .subscribe(sink::next, sink::error)
instead and no error:
@Test
void reproCase() {
AtomicReference<Throwable> throwableAtomicReference = new AtomicReference<>();
Hooks.onErrorDropped(throwableAtomicReference::set);
Hooks.onOperatorDebug();
try {
Flux.<Integer>create(sink -> {
AtomicInteger counter = new AtomicInteger(0);
while (counter.get() < 5) {
Mono.create((MonoSink<Integer> ms) -> {
if (counter.get() < 4) {
ms.success(counter.incrementAndGet());
} else {
counter.incrementAndGet();
ms.error(new RuntimeException());
}
})
.subscribe(sink::next, sink::error); // <<<
}
}).collectList().block();
} catch (Exception e) {
// ignore
}
Throwable droppedError = throwableAtomicReference.get();
if (droppedError != null) {
throw new AssertionError("`onError` called with no handler.", droppedError);
}
}
Your Environment
MacOSx, openJDK 21.
- Reactor version(s) used: 2023.0.1
Hi, @rozza!
What you see is expected behaviour since LambdaSubscriber
when there is no error handler just falls back on the default execution flow which is in case of error - to drop error.
Please see the following implementation details for more info.
@rozza feel free to reopen this issue if you think the documentation should be explicit on the mentioned behaviour
Cheers,
Oleh
To add a bit more to what @OlegDokuka just said, doOnError(...)
operator is meant for side-effects. The subscribe()
variant that accepts an error handler lambda has means to explicitly handle the error when propagated from the upstream chain, while the one where you don't pass it will drop the error wrapped in an exception that you observe. It has no notion of doOnError
operators along the way, as these are not meant for proper signal propagation, but allow to do some side processing when an error happens.
Hi @OlegDokuka & @chemicL,
Makes sense to me thanks for the explainations. My usecase added to the confusion by using nested sinks and signalling between them.
Cheers,
Ross