Flow error handling and launchIn
See original GitHub issueHere is a proposal to address the pattern of code that is commonly found in UI code that needs to launch a coroutine collecting a flow and updating UI such that:
uiScope.launch {
responseDataFlow().collect { updateDispaly(it) }
}
launchIn
The first piece is launchIn(scope)
operator that launches a coroutine collecting a flow and returns a Job
. Hereby I propose that this operator does not take any lambda, so that the replacement for the original code pattern is:
responseDataFlow()
.onEach { updateDisplay(it) }
.launchIn(uiScope)
This usage pattern maintains a consistent rule that execution context specifications in flows always work on “upstream” flows (like in flowOn
). Another reason for such a syntactic form will be apparent in the next sections.
onError
The most-generic basic handling operator is onError
. It catches exception that happens in flow before this operator is applied and pass the caught exception to the supplied lambda. Example:
responseDataFlow()
.onEach { updateDisplay(it) }
.onError { e -> showErrorMessage(e) } // catch errors in response flow and updateDisplay
.launchIn(uiScope)
Notice how onError
is written after onEach
to resemble the regular try/catch
code. Here it is important that collectIn
takes no lambda and cannot fail, so writing onError
before launchIn
always catches all exceptions.
Implementation note: onError
operator is already implemented in flow code, but now it is a private function called collectSafely
that is used internally to implement other error-handling operators.
onCompletion
This operator calls its lambda whenever a flow completes for any reason, essentially working as try/finally
:
responseDataFlow()
.onEach { updateDisplay(it) }
.onError { e -> showErrorMessage(e) }
.onCompletion { enableActionButtons() }
.launchIn(uiScope)
Advanced error handling
In addition to passing in an exception, onError
operator also has FlowCollector
receiver, which enables concise encoding of common error-handling patterns to replace an error with a value emitted to the flow. Some of those patterns are already provided as ready-to-use operators:
onErrorCollect(fallback: Flow<T>) = onError { emitAll(fallback) }
onErrorReturn(fallback: T) = onError { emit(fallback) }
TBD: Shall we rename them to onErrorEmitAll
and onErrorEmit
or leave them as is?
But onError
can be used directly for more advanced case. For example, if there is a flow of some Response
objects that support a Response.Failure
case that wraps exception, then one can easily translate an exception in the upstream stream to a failed response:
responseDataFlow()
.onError { e -> emit(Response.Failure(e)) }
.onEach { updateDisplay(it) } // failure in repose are encoded as values in the flow
.launchIn(uiScope)
Retry
For consistency I also propose to rename retry
to onErrorRetry
.
Open questions
-
Shall
onError
be configured with(Throwable)->Boolean
predicate that defaults to “always true” just like the otheronErrorXxx
operators? It is not ideal to have a two-lambda function, but predicate here is a “strategy” that might be stored in a separate variable, which actually might result in quite a readable code likeonError(applicationErrors) { e -> displayAppErrorMessage(e) }
-
onError
vsonCompletion
. One might envision a separate set ofonCompletionXxx
operators that are similar toonErrorXxx
but also perform the corresponding action on the normal completion of the flow, optionally accepting a(Throwable?)->Boolean
predicate. It is an open question if there are any use-cases to that.
Issue Analytics
- State:
- Created 4 years ago
- Comments:23 (14 by maintainers)
Yes. The goal is have more concise and more composable way to do
try/catch/finally
so that, for example, you can encapsulate error handling logic specific to your application as a flow operator and reuse it everywhere in a declarative way.Generally, using “bare”
try/catch/finally
in Kotlin is not very idiomatic. It is kind of “low-level” primitive that you usually find encapsulated inside higher-level operations. So far, coroutines are missing those “high-level” error-handling operations so you have to usetry/catch/finally
and the resulting code just looks “out-of-place” – a piece of imperative error-handling in sea of declarative code.I don’t see any easy way to enforce an order and I don’t see why it needs to enforced. It’s like
try/catch
. You catch error in the code in the abovetry
block. We can tweak the naming to make “modifying” nature ofonError
more explicit. We can even usecatch
(instead ofonError
) andfinally
(instead ofonCompletion
):(If go with this naming, we need to change the names of all the other
onErrorXxx
operators for consistency)Note, that you can use “error catching” operators multiple times to catch errors in differents parts of the flow: