Mister Smith
Mister Smith

Reputation: 28189

Kotlin: How to bypass CancellationException

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?

Upvotes: 6

Views: 5064

Answers (3)

Tenfour04
Tenfour04

Reputation: 93834

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.

Upvotes: 5

Roman  Elizarov
Roman Elizarov

Reputation: 28688

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)

Upvotes: 8

Tenfour04
Tenfour04

Reputation: 93834

I would consider this slightly cleaner syntax:

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

Upvotes: 2

Related Questions