Kotlin: How to bypass CancellationException
Asked Answered
C

3

6

I'm porting some old RxJava code to Coroutines. With RxJava I could do this in my activity:

someBgOperation()
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(MyActivity.this)))
.subscribe(
    MyActivity.this::onSuccess,
    MyActivity.this::onError
);

The autodispose library would cancel the Observable if the activity was being closed. In this case RxJava would not call the error handler, so it was possible to do UI-related operations in the error handler safely, such as showing a dialog.

Now in Kotlin we could have this equivalent code launched from lifecycleScope in the Activity, or in a viewModelScope if using ViewModel:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (e: Exception){
        //show dialog
    }
}

Both scopes are automatically cancelled when the activity closes, just what Autodispose does. But the catch block will execute not only with normal errors thrown by someBgOperation itself, but also with CancellationExceptions that are used by the coroutines library under the hood to handle cancellation. If I try to show a dialog there while the activity is being closed, I might get new exceptions. So I'm forced to do something like this:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (ce: CancellationException){
        //do nothing, activity is closing
    } catch (e: Exception){
        //show dialog
    }
}

This feels more verbose than the Rx version and it has an empty catch clause, which would show a warning in the lint output. In other cases where I do more things after the try-catch, I'm forced to return from the CancellationException catch to stay UI-safe (and those returns are tagged returns). I'm finding myself repeating this ugly template again and again.

Is there a better way of ignoring the CancellationException?

Cowey answered 5/6, 2020 at 16:43 Comment(0)
P
7

I can propose two solutions. First of all, the additional catch(e: CancellationException) clause looks a bit verbose. You can simplify the code to:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (e: Exception) {
        if (e !is CancellationException) // show dialog
    }
}

On the other hand, you can use Kotlin Flow whose catch operator is designed to ignore cancellations exactly for this purpose. Since you are not actually will be sending any values over the flow, your should use Flow<Nothing>:

flow<Nothing> {
    someBgOperation()
}.catch { e ->
    // show dialog
}.launchIn(viewModelScope)
Parlando answered 9/6, 2020 at 6:44 Comment(2)
Thanks for your answer. I agree, your try-catch is less verbose, but I'm not a fan of single line if's. If only a negative catch existed, like } catch (e: !CancellationException){.Cowey
The flow solution is beautiful. I personally prefer chaining over try-catch for error handling. I presume the porting from Rx would be easier as well.Cowey
T
5

Edit: revised, since CancellationExceptions should not be swallowed.

You could create a helper function that converts to a Result so you can handle only non-cancellation Exceptions:

public inline fun <T, R> T.runCatchingCancellable(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        if (e is CancellationException) {
            throw e
        }
        Result.failure(e)
    }
}

Usage:

viewModelScope.launch {
    runCatchingCancellable {
        someBgOperation()
    }.onFailure { e ->
        //show dialog
    }
}

And this function can serve as a safe alternative to runCatching to use in cancellable coroutines.

Timeworn answered 5/6, 2020 at 18:1 Comment(4)
You might consider building this out of runCatching instead of inventing your own wrappers.Cushman
I was already thinking on how this need could be solved with a little library or extension function. I wonder why Kotlin devs didn't though of it (maybe they don't catch exceptions at all!)Cowey
shouldn't you rethrow the CancellationException here?Nucellus
@Nucellus Yes, I revised it.Timeworn
T
2

I would consider this slightly cleaner syntax:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (e: Exception){
        if (isActive) {
            //show dialog
        }
    }
}
Timeworn answered 5/6, 2020 at 17:2 Comment(1)
It doesn't, it will still swallow the CancellationExceptionRoselleroselyn

© 2022 - 2024 — McMap. All rights reserved.