11

I have the following code:

val channel = BroadcastChannel<Event>(10)

fun setup() {
    scope.launch {
        channel.asFlow().
            .flatMapLatest { fetchSomeData() }
            .catch { emit(DefaultData()) }
            .onEach { handleData() }
            .collect()

    }
}

fun load() {
    channel.offer(Event.Load)      
}

In case fetchSomeData fails with an exception it will be caught by catch and some default data is passed on. The problem is that the flow itself gets canceled and is being removed from the subscribers of the channel. This means that any new events offered to the channel will be ignored since there are no longer any subscribers.

Is there a way to make sure the flow does not get cancelled in case of an exception?

gookman
  • 2,527
  • 2
  • 20
  • 28
  • It should at least be possible to ensure no exceptions escape from operators by using a plain `try-catch` block within them. However, when I tried it, i hit a Kotlin compiler bug. – Marko Topolnik Feb 04 '20 at 09:15
  • Exceptions don't really escape in this case. That is not the problem. – gookman Feb 04 '20 at 10:22
  • Exceptions do escape the operator, cancelling the flow. If you didn't let them escape the operator and handled them locally, then the flow would not get cancelled. – Marko Topolnik Feb 04 '20 at 11:05
  • Okay. Now I understand what you mean. Let's just assume that this is out of our control and `fetchSomeData` just returns a flow. I am looking for an approach similar to RxJava. – gookman Feb 04 '20 at 14:57
  • probably you need `fetchSomeData().catch { emit(DefaultData()) }` instead? – qwwdfsad Mar 11 '20 at 15:48
  • @gookman Did you have a progress on this? – jjz Nov 17 '20 at 21:33
  • @jzarsuelo check the accepted answer. – gookman Nov 18 '20 at 15:42
  • @gookman Thanks! That one worked though I don't get why do the catch is necessary in `fetchSomeData()`, I'm thought the `catch()` after `flatMapLatest` is enough. – jjz Nov 19 '20 at 09:15
  • 2
    I believe it's because `fetchSomeData` in `flatMapLatest` is running in a child coroutine scope. If the exception is being caught in the child scope, then it is handled there and not passed on to the parent scope. This is my understanding from skimming `kotlinx.coroutines.flow.internal.Merge.kt`. – gookman Nov 19 '20 at 09:37
  • Thanks! I should also check that part. I didn't expect the behaviour of the exception handling will be like this. – jjz Nov 20 '20 at 12:36

3 Answers3

6

You should catch exception of fetchSomeData(), so move catch from main flow to fetchSomeData():

    scope.launch {
        channel.asFlow().
            .flatMapLatest { fetchSomeData().catch { emit(DefaultData()} }
            .onEach { handleData() }
            .collect()

    }
gildor
  • 1,789
  • 14
  • 19
  • Extra information here: for this to work the exception needs to be thrown in the flow returned by `fetchSomeData`. If `fetchSomeData` itself returns an exception it will not be caught. – gookman Nov 18 '20 at 15:41
  • @gildor how does catching the exception in `fetchSomeData()` worked? I thought it cascade any exception to the main flow. – jjz Nov 19 '20 at 09:17
  • @jzarsuelo fetchSomeData returns flow, and by adding catch on it, every fail of this flow will be replaced with DefaultData(), so failed even will not be propagated to parent flow – gildor Nov 22 '20 at 14:52
-2

I faced the same issue. My workaround is something like this:

/* Custom onEach extension function */
fun <T> Flow<T>.onEachCatching(block: suspend (T) -> Unit) = OnEachCatching(this, block)

class OnEachCatching<T>(private val src: Flow<T>, private val block: suspend (T) -> Unit, bufferCapacity: Int = Channel.CONFLATED) {

    private val okValue = Channel<T>(bufferCapacity)

    private var failBlock: (suspend (Throwable) -> Unit)? = null

    init {
        GlobalScope.launch {
            src.collect { value ->
                runCatching { block(value) }
                    .onFailure { failBlock?.invoke(it) }
                    .onSuccess { okValue.send(value) }
            }

            okValue.close()
        }
    }

    fun onFailure(block: suspend (Throwable) -> Unit) = this.also {
        failBlock = block
    }

    fun resumeFlow() = okValue.consumeAsFlow()
}

Usage:

someData
    .onEachCatching { handleData() }
    .onFailure { emit(DefaultData()) }
    .resumeFlow()
    .collect()
whý
  • 162
  • 8
-3

My desigion is to simply restart collect flow

private fun startParsingMessages() {
    coroutineScope?.launch {
        sessionController.subscribeToMessages()
            .onEach { /*code block*/ }
            .catch {
                it.cause
                    ?.let { error -> Timber.e(error) }
                    ?: Timber.e("startSession(): ${it.message}")

                startParsingMessages() //here
            }
            .collect()
    }
}
P1NG2WIN
  • 764
  • 1
  • 9
  • 24