1. Introduction
In the previous article, we’ve covered the basic differences between Mono.just(), defer(), fromSupplier() and create() methods. If you haven’t seen it yet, I highly recommend you to take a while and check it out.
Although we’ve spent quite some time trying to understand the behavior and possible use cases for each method, it’s worth to mention that we focused on successful cases. Unfortunately, this is not always the case in real-life scenarios. Provided that, in this article we will focus on how the mentioned methods vary in exceptions and null handling and what should we be aware of.
2. Imports
As the first step, let’s import the necessary library to our project:
implementation("io.projectreactor:reactor-core:3.4.12")
3. Null Handling
After that, we can start can start practicing with nulls, also known as The Billion Dollar Mistake. Even though the Kotlin provides us a great support in null prevention, they can still occur- either on purpose (hopefully), or by mistake.
Given that, let’s prepare a function called nullReturningDateFetching:
private fun nullReturningDateFetching(): LocalDateTime? { Thread.sleep(500) println("GETTING DATE") return null }
As a word of explanation- we expect that the above code will cause the current thread to sleep, following by printing GETTING DATE and returning a null value.
3.1. Mono.just()
As the next step, let’s start with the Mono.just() method- just like in the previous article:
private fun monoJustNull(): Mono<LocalDateTime> = Mono.just(nullReturningDateFetching()) fun monoJustNullSubscription() { val myMono = monoJustNull() myMono.subscribe(::println) myMono.subscribe(::println) myMono.subscribe(::println) } fun monoJustNullInstantiation() { val myMono = monoJustNull() }
And let’s run both methods:
// monoJustNullSubscription GETTING DATE Exception in thread "main" java.lang.NullPointerException: value // monoJustNullInstantiation GETTING DATE Exception in thread "main" java.lang.NullPointerException: value
As we can clearly see, both cases completed with the same result- thrown NullPointerException. Whatsoever, if we used an IDE, like IntelliJ, it warned us about type mismatch! The reason behind that is pretty straightforward- internally, the Mono.just() method creates a MonoJust class instance, which requires a constructor argument to be not-null.
Additionally, we can’t implement any callback behavior using error-handling operators, like onErrorReturn(). The only possibility to recover in this case would be to wrap the Mono.just(nullReturningDateFetching()) with a try-catch block and personally, I find this solution a bit ugly.
Thankfully, if we really need to use the Mono.just() and the value we would like to wrap can be null, a Mono class ships with the justOrEmpty() method. Instead of throwing exception, it will emit an onComplete signal, which we can fallback with, for instance, defaultIfEmpty().
3.2. Mono.defer()
As the next example, let’s figure out how does the Mono.defer() works:
Note: the following examples will focus on the subscription part- the reason for this has been covered in the previous article.
fun monoDeferNull(): Mono<LocalDateTime> = Mono.defer { null } fun monoDeferSubscription() { val myMono = monoDeferNull() myMono.subscribe(::println) myMono.subscribe(::println) myMono.subscribe(::println) }
Nextly, let’s check the result:
// monoDeferSubscription // 3x: [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException: The Mono returned by the supplier is null reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException: The Mono returned by the supplier is null Caused by: java.lang.NullPointerException: The Mono returned by the supplier is null
As we might have noticed, the exception seems to be exactly the same. However, it has been caught internally and propagated into the onError signal. For that reason, the number of messages is equal to the number of Subscribers we’ve created.
Let’s take a while to see what happens underneath:
try { p = Objects.requireNonNull(supplier.get(), "The Mono returned by the supplier is null"); } catch (Throwable e) { Operators.error(actual, Operators.onOperatorError(e, actual.currentContext())); return; }
We can clearly spot, that this code is responsible for the message we’ve received. The Operators.error itself is responsible for calling the onError method of our Subscribers.
On the positive side, this behavior allows us to recover with some error-handling operator, like:
fun monoDeferSubscriptionRecover() { val myMono = monoDeferNull() .onErrorReturn(LocalDateTime.MIN) myMono.subscribe(::println) myMono.subscribe(::println) myMono.subscribe(::println) } // Result: -999999999-01-01T00:00 -999999999-01-01T00:00 -999999999-01-01T00:00
3.3. Mono.fromSupplier()
Nextly, let’s check the Mono.fromSupplier() method:
fun monoFromSupplierNull(): Mono = Mono.fromSupplier { nullReturningDateFetching() } fun monoFromSupplierSubscription() { val myMono = monoFromSupplierNull() myMono.subscribe(::println) myMono.subscribe(::println) myMono.subscribe(::println) }
Similarly, let’s see the result:
// monoFromSupplierSubscription GETTING DATE GETTING DATE GETTING DATE
This time, no exception has been thrown. In practice, the fromSupplier() method just completes empty when the provided Supplier ( nullReturningDateFetching() in our case) resolves to null. Although we got rid of exception, we need to be aware of that and handle it accordingly. Indeed, the Mono class ships with defaultIfEmpty method, which could be used here.
3.3. Mono.create()
As the last example of null handling, let’s see the Mono.create() behavior:
private fun monoCreateNull(): Mono = Mono.create { monoSink -> monoSink.success(nullReturningDateFetching()) } fun monoCreateNullSubscription() { val myMono = monoCreateNull() myMono.subscribe(::println) myMono.subscribe(::println) myMono.subscribe(::println) }
And again, let’s run the above code:
// monoCreateNullSubscription GETTING DATE GETTING DATE GETTING DATE
As we can see, result itself is exactly the same, as in the previous example. However, the reason is slightly different and pretty well explained in the documentation itself:
Calling this method [ success(T) ] with a null value will be silently accepted as a call to success() by standard implementations.
As a result, the silently accepted success() call completes without any value.
3. Exceptions Handling
With all of that being explained, we can finally focus on their differences in exceptions handling.
Just like previously, let’s start with another date fetching variant, called exceptionDateFetching:
private fun exceptionDateFetching(): LocalDateTime { Thread.sleep(500) println("GETTING DATE") throw RuntimeException("ERRORS HAPPEN") }
Unlike the previous paragraph, let’s perform a collective comparison of all methods:
private fun monoJustException(): Mono<LocalDateTime> = Mono.just(exceptionDateFetching()) private fun monoDeferException(): Mono<LocalDateTime> = Mono.defer { monoJustException() } private fun monoFromSupplierException(): Mono<LocalDateTime> = Mono.fromSupplier { exceptionDateFetching() } private fun monoCreateException(): Mono<LocalDateTime> = Mono.create { monoSink -> monoSink.success(exceptionDateFetching()) }
After running all of them, we should see the following result:
// monoJustExceptionSubscription GETTING DATE Exception in thread "main" java.lang.RuntimeException: ERRORS HAPPEN // monoDeferExceptionSubscription & monoFromSupplierExceptionSubscription & monoCreateExceptionSubscription // 3x GETTING DATE [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.RuntimeException: ERRORS HAPPEN reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.RuntimeException: ERRORS HAPPEN Caused by: java.lang.RuntimeException: ERRORS HAPPEN
As we can see- when using the Mono.just() method, we have to explicitly make sure that all exceptions are handled within the function responsible for date-time creation. Without that, the exception will be thrown on the instantiation and lead to thread termination.
On the other hand, the rest of the methods will log an error and finally, throw it via Exceptions.bubble(Throwable). In other words, the exception will be again caught internally and propagated as an onError signal. As we have already seen, this type of signal might be pretty easily handled with onError* methods.
5. Summary
And that would be all for this article. I really hope, that you will benefit from this, and the previous tutorial describing differences between the Mono.just(), defer(), fromSupplier() and create() methods. Although the Project Reactor is not the most trivial thing in the world, I am 100% sure that you can take advantage of learning it. It will be a pleasure to me to walk you through this process.
Finally, just like always, you can find the source code here. Moreover, if you would like to ask about anything or you have some suggestions about future topics, please let me know about it in the comment section below, or by using the contact form.