Mark Lapasa
Mark Lapasa

Reputation: 1654

Catch an Exception inside Kotlin callbackFlow builder

I have a consumer that looks like this:

// ItemConsumer.kt
try {
    job = itemService
            .connect()
            .flowOn(Dispatchers.IO)
            .catch { e ->
                throw e
            }
            .onEach {
                // Update UI for each item collected
            }
            .launchIn(viewModelScope)
} catch (e : Exception) {
    // Handle exception
}

And I have a producer that looks this:

// ItemService.kt (Producer)
fun connect():Flow<Item> = callbackFlow {
    check(1 == 0) // This is expected to throw an IllegalStateException
}

I understand that the .catch { } will handle any issues that would arise inside the Consumer's .collect { } or the .onEach { } block. But how can I throw the exception that happens inside the callbackFlow { } builder so that it may be caught in the .catch { } of the consumer?

Upvotes: 2

Views: 2385

Answers (2)

Blo
Blo

Reputation: 11978

catch operator catches only upstream exceptions (that is an exception from all the operators above catch, but not below it). In your code, any exception in onEach or collect (via launchIn) won't be catched. You need to move this operator below onEach to catch all exceptions.

This operator catches exceptions in the flow completion. It could be possible to catch them in callbackFlow builder only if the flow completes normally or exceptionally. As @SomeRandomITBoy said it will never be thrown by the flow builder. But since this builder uses a Channel under the hood, in order to complete the flow exceptionally, you have to close it manually with non-null cause:

abstract fun close(cause: Throwable? = null): Boolean

Keep in mind that might violates transparency: "Flows must be transparent to exceptions and it is a violation of the exception transparency to emit values in the flow { ... } builder from inside of a try/catch block". I might wrong but I think, since you use catch, this "preserves this exception transparency and allows encapsulation of its exception handling".

So the consumer could be as follows:

job = itemService.connect()
    .flowOn(Dispatchers.IO)
    .onEach {
        // update UI
    }
    .catch { e ->
        // handle exception
    }
    .launchIn(viewModelScope)

And the producer should close with an exception like this:

fun connect(): Flow<Item> = callbackFlow {
    try {
        check(1 == 0) // throws an exception
    } catch (e: Exception) {
        close(e) // close manually with exception
    }
}

Upvotes: 4

Some random IT boy
Some random IT boy

Reputation: 8457

You can't. The exception will never be thrown by the flow builder function. Flows are cold, meaning that no code is executed until collect is called. You'll always build a flow successfully even if the execution of collection yields to an exception.

Upvotes: 0

Related Questions