Search code examples
javaspring-webfluxreactive-programmingreactive

Handling an empty Mono


I am trying to write a reactive function that performs a certain task when the first Mono is non-empty, and a "fallback" task when the Mono is empty. This is what I have so far:

static Mono<String> init(String input) {
    if (input == null) {
        return Mono.empty();
    }

    if ("err".equals(input)) {
        throw new IllegalArgumentException("Some exception");
    }

    System.out.println("First Mono: " + input);
    return Mono.just("First Mono: " + input);
}

static Mono<Void> doesSomeStuff(String input) {
    System.out.println("Second Mono: " + input);
    return Mono.empty();
}

My first attempt was to specify a flatMap call that would handle the valid Mono scenario, and a switchIfEmpty call that would handle the empty Mono scenario

    /**
     * Wrong! The 3rd line should not be shown! Defer causes empty block to be lazily evaluated
     *
     * First Mono: Hello
     * Second Mono: First Mono: Hello
     * Second Mono: Empty
     */
    Mono<String> firstMono = init("Hello");
    firstMono.flatMap(WebfluxDemoApplication::doesSomeStuff)
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")))
            .block();

    /**
     * Correct! Only the empty branch is executed
     *
     * Second Mono: Empty
     */
    Mono<String> first2Mono = init(null);
    first2Mono.flatMap(WebfluxDemoApplication::doesSomeStuff)
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")))
            .block();

However, the output does not align with my expectations. I did some searching and I did find a sequence of operations that DOES work:

    /**
     * Correct!
     *
     * First Mono: Hello
     * Second Mono: First Mono: Hello
     */
    Mono<String> first5Mono = init("Hello");
    first5Mono.delayUntil(initVal -> doesSomeStuff(initVal))
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
            .then()
            .block();

    System.out.println("---");

    /**
     * Correct!
     *
     * Second Mono: Empty
     */
    Mono<String> first6Mono = init(null);
    first6Mono.delayUntil(initVal -> doesSomeStuff(initVal))
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
            .then()
            .block();

    System.out.println("---");

    /**
     * Correct! It throws the Exception.
     *
     * Second Mono: Empty
     */
    Mono<String> first7Mono = init("err");
    first7Mono.delayUntil(initVal -> doesSomeStuff(initVal))
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
            .then()
            .block();

Can someone explain to me why this works? Why does delayUntil work, but not flatMap? Why do I have to cast the Mono in the switchIfEmpty statement? I believe then is used to signal completion by emitting an empty Mono. Does this just work coincidentally, because doesSomeStuff return type is Mono<Void>?


Solution

  • flatMap doesn't work because you are always flat-mapping to an empty Mono. This means it will always return an empty Mono, causing switchIfEmpty to always "switch".

    delayUntil doesn't affect the original Mono's value in any way. It just makes it emit its value a little later.

    I do think flatMap is more readable though, so you could do something like this:

    mono.flatMap(x -> doesSomeStuff(x).thenReturn(x))
            .switchIfEmpty(Mono.defer(() -> doesSomeStuff("empty")).then(Mono.empty()))
            .block();
    

    thenReturn makes sure that flatMap doesn't "eat up" the value. then(Mono.empty()) is then used to convert the Mono<Void> into an empty Mono<String>.

    Your cast works here because doesSomeStuff always returns an empty Mono. I suppose you could call that a "coincidence". then() is not really relevant here - it just gets rid of the String value that you would have gotten if the original Mono does have a value. If doesSomeStuff returns a non-empty Mono, you'd need some other way to make it a Mono<String>, e.g. then(Mono.empty()).

    That said, if doesSomeStuff does return a non-empty Mono of another type, you can just flatMap and then switchIfEmpty like you did in your first attempt. In this case, flatMap will preserve the existence of a value.

    If you don't actually need the value of the original Mono to pass to doesSomeStuff, you can just use hasElement:

    mono.hasElement().flatMap( hasElem ->
        hasElem ? doesSomeStuff("intVal") : doesSomeStuff("Empty")
    ).block();