Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

The Mono is a reactive type provided by Project Reactor, representing either a single asynchronous value or no value. When working with Mono, determining whether it is empty helps with correct application behavior.

In this tutorial, we’ll explore how to check if a Mono is empty in Kotlin, leveraging JUnit test cases for demonstration.

2. Understanding Mono Emptiness and Mapping Behavior

A Mono represents either a single asynchronous value or no value (empty). When working with monos, it’s crucial to understand how they behave when empty:

  • Mapping Behavior: When a Mono is empty, any mapping function (flatMap(), map(), or similar) applied to it will not execute. This means that if we chain operations on a Mono, those operations will only run if the Mono emits a value.
  • Default or Fallback Handling: To handle cases where a Mono is empty, methods like defaultIfEmpty() or switchIfEmpty() allow us to define alternative behavior.

For example:

@Test
fun `mapping function is skipped for empty Mono`() {
    val callCount = AtomicInteger(0)
    val emptyMono = Mono.empty<String>()

    val resultMono = emptyMono.map { 
        callCount.incrementAndGet()
        it.uppercase() 
    }

    StepVerifier.create(resultMono)
        .verifyComplete() // No value emitted, Mono completes directly
    assertEquals(0, callCount.get())
}

This fundamental behavior ensures efficient handling of empty states and provides the foundation for more advanced techniques, such as defining default values or fallbacks.

When a Mono completes without emitting a value, it implicitly means that any mapping or transformation functions in the chain were skipped. While verifying completeness does not directly prove the mapper wasn’t called, combining it with additional tracking mechanisms like a counter or logging provides concrete evidence.

3. Checking Mono Emptiness with hasElement()

Using hasElement() allows us to determine whether a Mono emits a value. This method is useful when checking whether the Mono contains any value before proceeding with further operations.

First, let’s verify this function on an empty Mono:

@Test
fun `should detect empty Mono using hasElement`() {
    val emptyMono = Mono.empty<String>()

    StepVerifier.create(emptyMono.hasElement())
        .expectNext(false)
        .verifyComplete()
}

We created an empty Mono with Mono.empty() and verified that hasElement() emits false.

Now, let’s try with a Mono that has a value. We’ll use Mono.just() to start with a basic value:

@Test
fun `should detect non-empty Mono using hasElement`() {
    val nonEmptyMono = Mono.just("Hello, World!")

    StepVerifier.create(nonEmptyMono.hasElement())
        .expectNext(true)
        .verifyComplete()
}

Now, we see that hasElement() emits true when a value is present.

Calling the hasElement() method on a Mono creates a new Mono that emits a boolean indicating if the original Mono is empty. We can use StepVerifier to check the value emitted from our Mono and ensure it completes as expected.

4. Providing a Default Value with defaultIfEmpty()

The defaultIfEmpty() method provides a default value if the Mono is empty. This method is useful when we need a static fallback value rather than another Mono source. Again, we’ll test this with an empty Mono first:

@Test
fun `should provide default value for empty Mono`() {
    val emptyMono = Mono.empty<String>()

    val resultMono = emptyMono.defaultIfEmpty("Default Value")

    StepVerifier.create(resultMono)
        .expectNext("Default Value")
        .verifyComplete()
}

We created an empty Mono again with Mono.empty() and verified that resultMono emits our “Default Value”.

Let’s see what happens when we try with a Mono that has a value:

@Test
fun `should not override value for non-empty Mono`() {
    val nonEmptyMono = Mono.just("Actual Value")

    val resultMono = nonEmptyMono.defaultIfEmpty("Default Value")

    StepVerifier.create(resultMono)
        .expectNext("Actual Value")
        .verifyComplete()
}

Using defaultIfEmpty(), we can ensure that a predefined fallback value is only emitted when the Mono is empty.

5. Providing a Fallback with switchIfEmpty()

While defaultIfEmpty() is useful for providing static fallback values, switchIfEmpty() is more powerful and allows us to provide another Mono as a fallback source. This can be useful when the fallback value needs to be dynamically generated or involves asynchronous computation.

Let’s start by looking at handling an empty Mono with switchIfEmpty():

@Test
fun `should provide fallback value for empty Mono`() {
    val emptyMono = Mono.empty<String>()

    val fallbackMono = emptyMono.switchIfEmpty(Mono.just("Fallback Value"))

    StepVerifier.create(fallbackMono)
        .expectNext("Fallback Value")
        .verifyComplete()
}

First, we create an empty Mono with Mono.empty()The switchIfEmpty() method provides a fallback Mono that emits the string “Fallback Value”. When the original Mono is empty, the fallback Mono is the source for the resulting stream.

Finally, let’s understand how switchIfEmpty() works with a Mono that has a value:

@Test
fun `should not invoke fallback for non-empty Mono`() {
    val nonEmptyMono = Mono.just("Hello, World!")

    val resultMono = nonEmptyMono.switchIfEmpty(Mono.just("Fallback Value"))

    StepVerifier.create(resultMono)
        .expectNext("Hello, World!")
        .verifyComplete()
}

This time, our fallback value isn’t used, which is as we expected.

The switchIfEmpty() approach ensures that a meaningful default value is always available when the source Mono doesn’t emit data.

6. Conclusion

Handling empty Monos is a key aspect of reactive programming. Since Monos skip all mapping operations when empty, developers need explicit strategies to manage such cases. Methods like defaultIfEmpty() provide simple fallback values, while switchIfEmpty() enables more dynamic and reactive alternatives.

By mastering these techniques, we can design robust and efficient reactive flows that gracefully handle missing data.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.