Why does withContext await for the completion of child coroutines
Asked Answered
B

2

9

The documentation of withContext states

Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.

However, the actual behavior is that it awaits on all the child coroutines as well, and doesn't necessarily return the result of the block but instead propagates any exception in the child coroutine.

suspend fun main() {
    try {
        val result = withContext(coroutineContext) {
            launch {
                delay(1000L)
                throw Exception("launched coroutine broke")
            }
            println("done launching")
            42
        }
        println ("result: $result")
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
}

I would expect the above to print result: 42 and then, possibly, print the uncaught exception from the child coroutine. Instead it waits for one second and then prints Error: launched coroutine broke.

The actual behavior, therefore, matches that of the coroutineScope builder. While it may be a useful behavior, I think it contradicts the documentation. Should the documentation be updated to something similar to coroutineScope?

This function returns as soon as the given block and all its children coroutines are completed.

Furthermore, does that mean that we can use coroutineScope and withContext(coroutineContext) interchangeably, the only difference being a bit less boilerplate?

Bianca answered 5/7, 2019 at 13:30 Comment(2)
Somehow related questionZabaglione
With my answer on it :) Doesn't answer this question, though.Bianca
A
14

withContext creates a new job. This means that all coroutines launched inside are children of this job. It only returns when the job is finished. Because of structured concurrency, it only finishes when all child coroutines are finished too.

When any of the child jobs fails, the parent job is canceled. This will also cancel all other child jobs. Since withContext returns a result, the exception is thrown.

The documentation of CoroutineScope is helpful in this regards:

Every coroutine builder (like launch, async, etc) and every scoping function (like coroutineScope, withContext, etc) provides its own scope with its own Job instance into the inner block of code it runs. By convention, they all wait for all the coroutines inside their block to complete before completing themselves, thus enforcing the discipline of structured concurrency.

I think the documentation of withContext could be improved too. The documentation of Job and CoroutineContext are very helpful as they provide a more high-level point of view.

Furthermore, does that mean that we can use coroutineScope and withContext(coroutineContext) interchangeably, the only difference being a bit less boilerplate?

Yes, they should behave the same way. They are intended for different use cases though.

coroutineScope is meant to provide a scope for multiple parallel coroutines in which all will be canceled, if any fails.

withContext is designed to be used to switch the context (eg. the Dispatcher) for the given block of code.

Here is a similar question I recently asked on the kotlin discussion forums. The thread contains some more similar cases and further insight.

Anile answered 5/7, 2019 at 17:14 Comment(4)
Trouble is, I was using withContext before CoroutineScope even existed :) and its doc contains no reference to the fact that it creates its own job, scope etc. The sentence "suspends until it completes" is downright false because the block completes right away.Bianca
the difference is semantics only -- from the Kotlin forum thread i conclude it's the other way around, semantics are identical but the intended usage is different. So, even though you can use them interchangeably, you shouldn't because it's bad style.Bianca
Agreed, that documentation is incorrect. I guess you should open a ticket on the bugtracker.Anile
the difference is semantics only: Updated the answer to clarify. I wanted to express what you said. Guess I used the word semantics incorrectly.Anile
C
0

The behavior of withcontext is correct. Think of it like if an exception occurs the code below anyway will no execute until and unless you have handled the exception at that place itself and not at the function level.

As you are launching a coroutine inside withcontext coroutinescope function, we can do something like this,

var job = launch { delay(1000L) throw Exception("launched coroutine broke") } job.invokeOnCompletion { exception -> println("Caught $exception") }

job.join()

Basically it will handle the exception and will not propagate to the parent coroutine. or create a CoroutineExceptionHandler and pass it to the launch(CoroutineExceptionHandler ) to handle it there itself.

Circosta answered 3/9, 2022 at 4:28 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Rubric

© 2022 - 2024 — McMap. All rights reserved.