Coroutine with a SupervisorJob - cancellation behaviour

I need to implement some exception handling in my code so I've the following coroutine test code which launched from a fragment;

private val scoped = CoroutineScope(Dispatchers.Default + SupervisorJob())

...

val handler = CoroutineExceptionHandler { _, exception ->
        println("TAG-Caught $exception")
    }

scoped.launch(handler) {

        val job1 = launch {
            println("TAG-first job is running")
            delay(200)
        }

        testParentChildWithExceptionWithSupervision()

        launch {
            println("TAG-third job is running")
        }
    }

where the method testParentChildWithExceptionWithSupervision looks like;

suspend fun testParentChildWithExceptionWithSupervision() {

    supervisorScope {

        val job1 = launch {
            println("TAG-running first (inner) child")
            delay(200)
            throw ArithmeticException()
        }

        val job2 = launch {
            job1.join()
            println("TAG-First child is cancelled: ${job1.isCancelled}, but second one is still active")
            delay(200)
        }

        job1.join()
        println("TAG-second child is cancelled: ${job2.isCancelled}")

        println("TAG-ENDING")
    }
}

The output is as I expected;

enter image description here

The thing is as soon as I change supervisorScope to coroutineScope in the suspending function, I see that root scope (with the SpervisorJob) do not carry on with her childs;

suspend fun testParentChildWithExceptionWithoutSupervision() {

        coroutineScope {

            val job1 = launch {
                println("HH-doing first child")
                delay(200)
                throw ArithmeticException()
            }

            val job2 = launch {
                job1.join()
                println("HH-First child is cancelled: ${job1.isCancelled}, but second one is still active")
                delay(200)
            }

and I get this output instead;

enter image description here

So nothing seems to be carried on in the root scope after having an exception even the scope has a supervisor job. I bet I miss sth but not able to see it. Can someone please explain the reason behind it?


Solution 1:

If you check out the documentation on suspend fun coroutineScope, you'll find this:

The method may throw a [...] corresponding unhandled Throwable if there is any unhandled exception in this scope (for example, from a crashed coroutine that was started with launch in this scope).

This is the case that happens in your code: the "first (inner) child" crashes with an unhandled ArithmeticException. This becomes the result of testParentChildWithExceptionWithSupervision and at the call site, nothing handles it. Therefore it proceeds to crash the parent as well — not through the mechanism of propagating coroutine cancellation, but through the basic exception mechanism. SupervisorJob makes no difference here, the main block of code completes abruptly with that exception unhandled, which is why you see it printed by the unhandled exception handler.

If you modify your code to do this:

    try {
        testParentChildWithExceptionWithSupervision()
    } catch (e: ArithmeticException) {
        println("ArithmeticException in main block")
    }

you'll see the main coroutine proceed until the end.