Arst
Arst

Reputation: 3307

Exception is not caught in the launch block in a coroutineScope

In the test2, I found the exception is not caught even if I pass an exception handler. It only works I launch a coroutine to wrap the call like the test1... my expectation is the first runSuspend call can catch the exception and the code can continue to do the second runSuspend call.

package coroutine.exceptions

import kotlinx.coroutines.*

fun log(msg: String) = println("$msg (${Thread.currentThread().name})")
val exceptionHandler = CoroutineExceptionHandler { _, e ->
    log(e.localizedMessage)
}

fun main() = runBlocking {

    val test1 = TestReuseCoroutineAfterException2("test1")
    test1.launch(exceptionHandler) {
        test1.runSuspend(true)
        // below won't be run because an exception is thrown above.
        delay(2000)
        test1.runSuspend(false)
        delay(2000)
    }.join()
    println()

    val test2 = TestReuseCoroutineAfterException2("test2")
    test2.runSuspend(true)
    // below won't be run because an exception is thrown above.
    delay(2000)
    test2.runSuspend(false)
    delay(2000)

    log("finished")
}


class TestReuseCoroutineAfterException2(private val testName: String) :
    CoroutineScope by CoroutineScope(Dispatchers.Default) {

    suspend fun runSuspend(throwException: Boolean) = coroutineScope {
        log("$testName: callSuspend - started")
        launch(exceptionHandler) {
            if (throwException)
                throw Exception("$testName: callSuspend - throw exception")
            else
                log("$testName: callSuspend - done")
        }
        log("$testName: callSuspend - ended")
    }
}

Output

test1: call

Suspend - started (DefaultDispatcher-worker-2)
test1: callSuspend - ended (DefaultDispatcher-worker-2)
test1: callSuspend - throw exception (DefaultDispatcher-worker-1)

test2: callSuspend - started (main)
test2: callSuspend - ended (main)
Exception in thread "main" java.lang.Exception: test2: callSuspend - throw exception
    at coroutine.exceptions.TestReuseCoroutineAfterException2$callSuspend$2$1.invokeSuspend(TestReuseCoroutineAfterException2.kt:40)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at coroutine.exceptions.TestReuseCoroutineAfterException2Kt.main(TestReuseCoroutineAfterException2.kt:10)
    at coroutine.exceptions.TestReuseCoroutineAfterException2Kt.main(TestReuseCoroutineAfterException2.kt)

Process finished with exit code 1

Upvotes: 0

Views: 472

Answers (1)

ardenit
ardenit

Reputation: 3890

CoroutineScope created via coroutineScope builder fails if its child coroutine fails. If you don't want your scope to fail in this case, use supervisorScope builder:

suspend fun runSuspend(throwException: Boolean) = supervisorScope {

With this change your code prints:

test1: callSuspend - started (DefaultDispatcher-worker-1)
test1: callSuspend - ended (DefaultDispatcher-worker-1)
test1: callSuspend - throw exception (DefaultDispatcher-worker-1)
test1: callSuspend - started (DefaultDispatcher-worker-1)
test1: callSuspend - ended (DefaultDispatcher-worker-1)
test1: callSuspend - done (DefaultDispatcher-worker-1)

test2: callSuspend - started (main)
test2: callSuspend - ended (main)
test2: callSuspend - throw exception (main)
test2: callSuspend - started (main)
test2: callSuspend - ended (main)
test2: callSuspend - done (main)
finished (main)

Upvotes: 3

Related Questions