Search code examples
kotlinkotlin-coroutineskotlin-flow

Kotlin Flow with catch operator still completes


I am having trouble understanding how the catch operator works in a kotlin Flow. Here is the catch documentation

Questions:

  1. Why doesn't the presence of a catch allow the Flow to continue upon encountering an exception, rather than complete?
  2. Placement of the catch operator seems to change the behavior. Why can't I place the catch operator at the end of the chain to see the same result? In my example, it only executes if I place it BEFORE onEach.

Example gist

First example, placing the catch BEFORE onEach:

fun main() {
    // Flow of lambdas that return a String (or throw an Exception)
    flowOf<() -> String>({ "Hello " }, { error("error") }, { "World" })
        // Map to the result of the invocation of the lambda
        .map { it() }
        // This line will emit the error String, but then the flow completes anyway.
        // I would expect the flow to continue onto "World"
        .catch { emit("[Exception caught] ") }
        .onEach { println(it) }
        .launchIn(GlobalScope)
}

Actual result: Hello [Exception caught]

Expected result: Hello [Exception caught] World

Second example, placing the catch AFTER onEach:

fun main() {
    // Flow of lambdas that return a String (or throw an Exception)
    flowOf<() -> String>({ "Hello " }, { error("error") }, { "World" })
        // Map to the result of the invocation of the lambda
        .map { it() }
        .onEach { println(it) }
        // I would expect this catch to emit, but it never gets here.
        .catch { emit("[Exception caught] ") }
        .launchIn(GlobalScope)
}

Actual result: Hello

Expected result: Hello [Exception caught] World

Or, since the onEach happens before the catch's emission, the catch's emission would be ignored? In which case, the expected output would be this?: Hello World


Solution

  • Easiest way for me to explain what you are doing there is to simplify it into syncronous code. You are basically doing this:

    fun main() {
        val list = listOf("Hello", "error", "World")
        
        try {
            for (s in list) {
                if (s == "error") error("this is the error message here")
                println(s)
            }
        } catch (e: Exception) {
            println("the exception message is: ${e.localizedMessage}")
        }
    }
    

    output:

    Hello
    the exception message is: this is the error message here
    

    As you can see, the exception is caught, but it can't prevent the stop of the for loop. The same way in which that exception stops the map function.

    Flow.catch will catch an exception and stop it from propagating (unless you throw it again), but it can't go a step back (to the map fun) and tell it to magically start again from where the next element would have been or etc.

    If you want that, you need to put a normal try/catch inside the .map fun. So it would be:

    fun main() {
        val list = listOf("Hello", "error", "World")
    
    
        for (s in list) {
            try {
                if (s == "error") error("this is the error message here")
                println(s)
            } catch (e: Exception) {
                println("the exception message is: ${e.localizedMessage}")
            }
        }
    }
    

    Output:

    Hello
    the exception message is: this is the error message here
    World
    

    The way Flow.catch is usually used is to catch an exception that would prevent the next step, like if you have:

    //pseudo
    
    flow
        .map{/*code*/}
        .filterNotNull()
        .doSomethingRisky() //this can throw an exception
        .catch {} //do something about it
        .doSomethingElse()
    

    In this scenario, even if doSomethingRisky throws an exception, the flow will still get to the doSomethingElse. This is more or less the use of the Flow.catch.