I am having trouble understanding how the catch
operator works in a kotlin Flow
.
Here is the catch documentation
Questions:
catch
allow the Flow
to continue upon encountering an exception, rather than complete?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
.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
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
.