I am experimenting a bit with flows in kotlin and asked myself a question: Will my flows be cancelled if one of the operations within the flow throws an exception even If I use .catch?
If not, how can I cancel my flow when an exception occurs even while using .catch?
Example
fun testFlow() = flow {
emit("Test")
emit(Exception("Error"))
emit("Test2") // This should not be emitted
}.catch { e -> println("Showing UI $e") }
Another Example
fun testFlow2() = flow {
emit("Test")
throw Exception("Error")
emit("Test2") // This should not be emitted
}.catch { e -> println("Showing UI $e") }
If the execution of the Flow throws an Exception, it will cancel and complete the Flow during collection. The collect() function call will throw the Exception if the Flow.catch operator was not used.
If you emit an Exception like in your example, it's just another object in the Flow. Since you have not specified the Flow's type, it's implicitly choosing a type that's common between String and Exception. I think you have a Flow<Serializable> since that's a common supertype of both. If you had specified Flow<String>, it would not allow you to emit an Exception.
Related
I need to collect only the first value from two emitted by flow.
I have a function that returns flow:
fun myFlow = flow {
try {
emit(localDataSource.fetchData())
} catch(e: Exception) {
// just skip this error
}
emit(remoteDataSource.fetchData(1000, 0))
}
In one special case I need only first emitted value, doesn't matter is it from local cache or remote source.
I tried this one:
fun getRandomFavoriteItem() = myFlow.first().filter { it.score > 7 }.randomOrNull()
But first() invocation always throws
java.lang.IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed, but then emission attempt of value.
What I've tried:
single() -
java.lang.IllegalArgumentException: Flow has more than one element
take(1).first() -
java.lang.IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed, but then emission attempt of value
Catch error but it doesn't stop here:
myFlow.catch { e ->
if (e !is IllegalArgumentException) {
throw e
}
}.first().filter { it.score > 7 }.randomOrNull()
My questions are:
What is the point of usage first() if it doesn't work in case of more than 1 emitted values? If I would know that my flow produces only one value I could just use any other terminal operator.
How to avoid those errors and how to collect only first value without adding repeated code?
This isn't an error in first(). It's an error in your flow. You are not permitted to swallow all exceptions in a Flow in the way you have.
Some varying approaches may differ in whether they detect that error, but what you must fix is how you "just skip" all exceptions. Consider catching only the specific exceptions you're concerned about, or at least making sure to catch and rethrow CancellationException or its subclasses.
Lous Wasserman already found the problem, here some more details.
As mentioned in the error message you're also catching the AbortFlowException.
java.lang.IllegalStateException: Flow exception transparency is
violated: Previous 'emit' call has thrown exception
kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted,
no more elements needed, but then emission attempt of value.
You're bascically catching an exception which interferes with the way flows work. The problem is not about the first function.
Since AbortFlowException is internal you cannot access it, but you can access its superclass CancellationException. You need to modify your catch block like this:
try {
emit(localDataSource.fetchData())
} catch (e: Exception) {
if(e is CancellationException) {
throw e
}
}
Now first will work in the way you expect it to.
Edit:
A better solution would be to handle the exception within fetchData (you might return null in case one was thrown). This way you don't get in the way of the flow mechanics.
If that is not possible, you could create a wrapper function which takes care of the exception handling.
Not sure how to handle checked exception in the Mono flow.
return Mono.when(monoPubs)
.zipWhen((monos) -> repository.findById(...))
.map((tuple) -> tuple.getT2())
.zipWhen((org) -> createMap(org))
.map((tuple) -> tuple.getT2())
.zipWhen((map) -> emailService.sendEmail(...))
.flatMap(response -> {
return Mono.just(userId);
});
Here, the sendEmail method is declared with throws Exception.
public Mono<Boolean> sendEmail(...)
throws MessagingException, IOException
So, How to handle this checked exception in the zipWhen flow.
Also, How to handle
.zipWhen((map) -> emailService.sendEmail(...))
if the method returns void.
You need to review implementation of the sendEmail. You cannot throw checked exceptions from the publisher and need to wrap any checked exception into an unchecked exception.
The Exceptions class provides a propagate method that could be used to wrap any checked exception into an unchecked exception.
try {
...
}
catch (SomeCheckedException e) {
throw Exceptions.propagate(e);
}
As an alternative, you could use lombok #SneakyThrows to wrap non-reactive method.
Exceptions are thrown from Mono method, you can use onError* methods to handle the exception the way you like
var result = Mono.just("test")
.zipWhen((map) -> sendEmail())
.onErrorMap(SendEmailException.class, e -> new RuntimeException(e.getMessage()))
.flatMap(Mono::just);
Also it is not very clear from your post that if sendEmail takes param or not, sendEmail is not taking any input, I would just use doOnNext as it is a void method.
Say I have an API like so:
interface Foo {
val barFlow: Flow<Bar>
}
And I consume it like so:
class FooConsumer(private val foo: Foo) {
init {
CoroutineScope(Dispatchers.IO).launch {
val bar = foo.barFlow.single()
println("Collected bar: $bar)
}
}
}
According to the docs for single a NoSuchElementException can be thrown if the flow is empty. However, this confuses me quite a lot, as a terminal operation on a flow will "await" elements of the flow to be emitted. So how will the call to single know that there were no elements in the flow? Maybe an element just hasn't been emitted yet?
I mean under the hood, the call to single is collecting the source flow before it does the check. Therefore at least 1 item must have been emitted before the check for null is carried out, so that null check should never succeed and a NoSuchElementException should never be thrown (for the case where the flow is of a non nullable type).
So will NoSuchElementException only be a possibility for flows of nullable types?
Here is the source code for single:
/**
* The terminal operator, that awaits for one and only one value to be published.
* Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow
* that contains more than one element.
*/
public suspend fun <T> Flow<T>.single(): T {
var result: Any? = NULL
collect { value ->
if (result !== NULL) error("Expected only one element")
result = value
}
if (result === NULL) throw NoSuchElementException("Expected at least one element")
#Suppress("UNCHECKED_CAST")
return result as T
}
NoSuchElementException is thrown when the Flow finishes its emission without emitting a single element. One case I can think of right now is when you need to turn a collection into a Flow source. If that collection is empty and you call single on that Flow you will get a NoSuchElementException.
This example may seem absurd but you get the point:
val emptyListFlow = emptyList<Int>().asFlow()
launch {
val data = emptyListFlow.single()
}
In my case, I made a list.first(), where the list was empty
I'm still learning Kotlin and I just learned about the "use" and how it is a replacement for a try, catch and finally block.
However I am curious if it is possible to customize it's exception handling for example:
var connection: Connection? = null
try {
connection = dataSource.connection
connection.prepareStatement(query).execute()
} catch (e: SQLException) {
logger.log("Specific error for that query")
e.printStackTrace()
} finally {
if (connection != null && !connection.isClosed) {
connection.close()
}
}
That code is my current one, I have a specific error I would like to display on the catch, would that be possible using use?
This is my current use code:
dataSource.connection.use { connection ->
connection.prepareStatement(query).execute()
}
As commented by #Tenfour04, and from the documentation
[use] Executes the given block function on this resource and then closes it down correctly whether an exception is thrown or not.
In particular it is implemented like this:
public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
this.closeFinally(exception)
}
}
That piece of code should look familiar if you're a Java developer, but basically it executes block passing this (i.e. the receiver object) as an argument to your block of code. At the end it closes the AutoCloseable resource. If at any point an exception is thrown (either inside block or while closing the resource), that exception is thrown back to the caller, i.e. your code.
As an edge case you could have 2 exceptions, one when executing block and one when closing the resource. This is handled by closeFinally (whose source is available in the same file linked above) and the exception thrown while closing the resource is added as a suppressed exception to the one thrown from block – that's because only up to 1 exception can be thrown by a method, so they had to choose which one to throw. The same actually applies to the try-with-resources statement in Java.
The following test succeeds with Process finished with exit code 0. Note, this test does print the exception to the logs, but does not fail the test (which is the behavior I want).
#Test
fun why_does_this_test_pass() {
val job = launch(Unconfined) {
throw IllegalStateException("why does this exception not fail the test?")
}
// because of `Unconfined` dispatcher, exception is thrown before test function completes
}
As expected, this test fails with Process finished with exit code 255
#Test
fun as_expected_this_test_fails() {
throw IllegalStateException("this exception fails the test")
}
Why do these tests not behave the same way?
Compare your test with the following one that does not use any coroutines, but starts a new thread instead:
#Test
fun why_does_this_test_pass() {
val job = thread { // <-- NOTE: Changed here
throw IllegalStateException("why does this exception not fail the test?")
}
// NOTE: No need for runBlocking any more
job.join() // ensures exception is thrown before test function completes
}
What happens here? Just like the test with launch, this test passes if you run it, but the exception gets printed on the console.
So, using launch to start a new coroutine is very much like using thread to start a new thread. If it fails, the error gets handled by uncaught exception handler in thread and by CoroutineExceptionHandler (see it in the docs) by launch. Exceptions in launch are not swallowed, but are handled by the coroutine exception handler.
If you want exception to propagate to the test, you shall replace launch with async and replace join with await in your code. See also this question: What is the difference between launch/join and async/await in Kotlin coroutines
UPDATE: Kotlin coroutines had recently introduced the concept of "Structured Concurrency" to avoid this kind of exception loss. The code in this question does not compile anymore. To compile it, you'd have to either explicitly say GlobalScope.launch (as in "I confirm that it Ok to loose my exceptions, here is my signature") or wrap the test into runBlocking { ... }, in which case exception is not lost.
I was able to create an exception throwing CoroutineContext for tests.
val coroutineContext = Unconfined + CoroutineExceptionHandler { _, throwable ->
throw throwable
}
Though this would probably not be suitable for production. Maybe need to catch cancellation exceptions or something, I'm not sure
A custom test rule so far seems to be the best solution.
/**
* Coroutines can throw exceptions that can go unnoticed by the JUnit Test Runner which will pass
* a test that should have failed. This rule will ensure the test fails, provided that you use the
* [CoroutineContext] provided by [dispatcher].
*/
class CoroutineExceptionRule : TestWatcher(), TestRule {
private val exceptions = Collections.synchronizedList(mutableListOf<Throwable>())
val dispatcher: CoroutineContext
get() = Unconfined + CoroutineExceptionHandler { _, throwable ->
// I want to hook into test lifecycle and fail test immediately here
exceptions.add(throwable)
// this throw will not always fail the test. this does print the stacktrace at least
throw throwable
}
override fun starting(description: Description) {
// exceptions from a previous test execution should not fail this test
exceptions.clear()
}
override fun finished(description: Description) {
// instead of waiting for test to finish to fail it
exceptions.forEach { throw AssertionError(it) }
}
}
I'm hoping to improve it via this post though
UPDATE: just use runBlocking - like Roman suggests.