Exceptions in Kotlin Flows

Last updated 35 days ago by Roman Elizarov


Conceptually Kotlin’s Flow<T> type represents an asynchronous cold stream¹ of elements of type T that can complete successfully or with an exception. Let us see how these exceptions can be handled and what we can learn about flows and exceptions from the basic principles.

Suppose that we are writing a UI application that displays an updating stream of values in UI and thus collects them from a flow. This application has a uiScope that is a [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html?source=post_page---------------------------) which lifetime is bound to the corresponding UI element that is displaying the data. There is a dataFlow() function that returns a flow with the data to be displayed and so the data display can be activated like this:

uiScope.launch **{** // launch a UI display coroutine dataFlow().collect **{** value -> updateUI(value) **} }**

Flow guarantees that updateUI is always called in the collector’s execution context which is defined by uiScope here. Even if dataFlow() is using a different context internally this fact does not leak from it in any way².

But what happens if there is an error in the dataFlow()? In this case, collect call throws an exception, which leads to exceptional completion of the coroutine, which gets propagated to the uiScope and, usually, will end up calling an uncaught exception handler ([CoroutineExceptionHandler](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html?source=post_page---------------------------)) in its context. This is fine if exception was truly unexpected and should never happen in correct code, but what if dataFlow(), for example, is reading data from the network and a failure will quite expectedly happen when there is something wrong with the network? It needs to be handled. The failure is reported via an exception and can be handled just like exceptions are normally handled in Kotlin — using try/catch block³:

uiScope.launch **{** **try** { dataFlow().collect **{** value -> updateUI(value) **}** } **catch** (e: Throwable) { showErrorMessage(e) } **}**

If we encapsulate this exception-handling logic into an operator on the flow then we can simplify this code, reduce nesting, and make it more readable:

uiScope.launch **{** dataFlow() .handleErrors() // handle dataFlow errors .collect **{** value -> updateUI(value) **} }**

But how can we implement this handleErrors function? A naive attempt to write it is shown below:

**fun** <T> Flow<T>.handleErrors(): Flow<T> = flow **{** **try** { collect **{** value -> emit(value) **}** } **catch** (e: Throwable) { showErrorMessage(e) } **}**

This implementation collects values from the upstream flow it is called on and emits them downstream, wrapping the collect call into the try/catch block just as we did before. It simply abstracts the code we initially wrote. Would it work? Yes, for this particular case. So why exactly this implementation is naive?

Read full Article