When an exception occurs in a coroutine, does it p...
# coroutines
d
When an exception occurs in a coroutine, does it propagate the exception to the parent first, or does the cancellation propagate to its children first? I tested the code below, and the termination order always changes depending on the position of the
delay()
functions. So, I couldn’t tell which happens first, exception propagation or cancellation propagation. I realized that depending on the coroutine’s progress, it’s impossible to know which one will terminate first. But, I’m still curious whether, at the exact moment an error occurs, the coroutine internally attempts to propagate cancellation to its children first or propagate the exception to its parent first.
Copy code
fun main(): Unit = runBlocking {
    val job1 = launch(CoroutineName("Coroutine1")) {

        val job2 = launch(CoroutineName("Coroutine2")) {
            val job4 = launch(CoroutineName("Coroutine4")) {
                println("Coroutine4 run")
                delay(600)
            }
            job4.invokeOnCompletion {
                println("Coroutine4 end")
            }
            val job5 = launch(CoroutineName("Coroutine5")) {
                println("Coroutine5 run")
                delay(600)
            }
            job5.invokeOnCompletion {
                println("Coroutine5 end")
            }
            println("Coroutine2 run")
        }
        job2.invokeOnCompletion {
            println("Coroutine2 end")
        }

        val job3 = launch(CoroutineName("Coroutine3")) {
            val job6 = launch(CoroutineName("Coroutine6")) {
                delay(600)
                println("Coroutine6 run")
            }
            job6.invokeOnCompletion {
                println("Coroutine6 end")
            }
            println("Coroutine3 run")
            delay(500)
            throw Exception() // here! exception occurs!
        }

        job3.invokeOnCompletion {
            println("Coroutine3 end")
        }
        println("Coroutine1 run")
    }

    job1.invokeOnCompletion {
        println("Coroutine1 end")
    }

}
👀 2
d
We are intentionally leaving this unspecified. Please note, though, that
invokeOnCompletion
does not get invoked whenever a coroutine gets cancelled: instead, it gets invoked when the coroutine finishes its work. This is an important distinction whenever there's some cleanup code: https://pl.kotl.in/Jnrbnw2Yi Also,
invokeOnCompletion
is a niche API that's mostly useful for implementing coroutine-aware concurrent data structures. It's almost always a good idea to avoid it in straightforward production code. See https://github.com/Kotlin/kotlinx.coroutines/issues/4180
d
@Dmitry Khalanskiy [JB] Can you tell me why do you intentionally leave this unspecified? I’m so curious what reasons or issues keep you from specifying it. Is it an issue related to multithreading?
d
Yes, the most obvious issue is multithreading, which prevents relying on these details. However, even single-threaded code that relies on these handlers being invoked in a specific order is basically unreadable, and we don't encourage writing such code.
d
@Dmitry Khalanskiy [JB] So, to summarize, in production code that follows ordinary structured concurrency (not using invokeOnCompletion), is it correct to assume that it’s intentionally left unspecified whether cancellation is propagated to child first or the exception is propagated to the parent first—because multithreading makes the order inherently non-deterministic?
👌 1
@Dmitry Khalanskiy [JB] Thank you so much 👍