reactor / reactor-core

Non-Blocking Reactive Foundation for the JVM

Home Page:http://projectreactor.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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