Jay Lee
Jay Lee

Reputation: 1902

Catch exception in Kotlin async coroutines and stop propagating

I want to catch an exception that is thrown from async coroutines. The following code demonstrates a problem:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        println(failedConcurrentSum())
    } catch (e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum() = coroutineScope {
    try {
        val one = async {
            try {
                delay(1000L)
                42
            } finally {
                println("First child was cancelled")
            }
        }

        val two = async<Int> {
            println("Second child throws an exception")
            throw ArithmeticException()
        }

        one.await() + two.await()
    } catch (e: ArithmeticException) {
        println("Using a default value...")
        0
    }
}

This prints:

Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException

The try-catch inside the failedConcurrentSum doesn't handle the exception thrown by val two. I can convince myself that this is due to the "structured concurrency".

However, this doesn't explain why wrapping the async's inside a coroutineScope catches the exception:

suspend fun failedConcurrentSum() = coroutineScope {
    try {
        val one = coroutineScope {
            async {
                try {
                    delay(1000L)
                    42
                } finally {
                    println("First child was cancelled")
                }
            }
        }

        val two = coroutineScope {
            async<Int> {
                println("Second child throws an exception")
                throw ArithmeticException()
            }
        }

        one.await() + two.await()
    } catch (e: ArithmeticException) {
        println("Using a default value...")
        0
    }
}

This prints:

First child was cancelled
Second child throws an exception
Using a default value...
0

Why does the latter catches an exception whereas the first doesn't?

Upvotes: 6

Views: 2520

Answers (2)

Marko Topolnik
Marko Topolnik

Reputation: 200158

coroutineScope is just a function, it sets up its scope internally and from the outside it always completes like a regular function, not messing with any outer scopes. This is because it doesn't leak any concurrent coroutines started within its scope. You can always reliably catch and handle exceptions thrown by coroutineScope.

async, on the other hand, completes immediately after launching the coroutine, therefore you have two concurrent coroutines: one running the async code and another calling the corresponding await. Since the async one is also the child of the one calling await, its failure cancels the parent before the parent's await call completes.

The try-catch inside the failedConcurrentSum doesn't handle the exception thrown by val two.

It actually does, if it gets the chance. But since the try-catch block is in a coroutine that runs concurrently to the one completing val two's Deferred, it just doesn't get the chance to do so before being cancelled due to the failure of the child coroutine.

Upvotes: 3

IR42
IR42

Reputation: 9682

coroutineScope uses Job

By default, a failure of any of the job’s children leads to an immediate failure of its parent and cancellation of the rest of its children. Job

You can use supervisorScope instead of coroutineScope

A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children. SupervisorJob

but you have to wait for the completion of the first async block.

Use coroutineScope inside try catch to return a default value immediately when an exception occurs

suspend fun failedConcurrentSum() = try {
    coroutineScope {
        val one = async {
            try {
                delay(1000L)
                42
            } finally {
                println("First child was cancelled")
            }
        }

        val two = async<Int> {
            println("Second child throws an exception")
            throw ArithmeticException()
        }

        one.await() + two.await()
    }
} catch (e: ArithmeticException) {
    println("Using a default value...")
    0
}

Upvotes: 2

Related Questions