Coroutine scope function and Error Boundary
coroutineScope: The hero of coroutines.
coroutineScope
creates a boundary that establishes a new CoroutineScope
.
The new scope inherits its coroutineContext
from the outer scope, but overrides the context’s Job
. It also causes the current coroutine to suspend until all child coroutines have finished their execution.
The scope created by coroutineScope
has a new Job - this creates an exception boundary where failures surface. However, exceptions will continue propagating up the Job hierarchy unless intercepted by exception handling (try-catch, CoroutineExceptionHandler) at the scope boundary.
Coroutine scope functions create an error/exception boundary: Allow us to try-catch the Exception to prevent it from bubbling up to parent Job.
Error boundary example
Without error boundary
launch { // Job A
launch { // Job B - direct child of A
throw Exception() // Exception happens in B
// B fails and cancels A
// NO stopping point at B
// Cancels A
}
}
Moving towards error boundary using coroutineScope
launch { // Job A
coroutineScope { // Job B - creates exception boundary
launch { // Job C - child of B
throw Exception() // Exception happens in C
// C fails, B fails and surfaces exception
// CAN be caught at B's boundary (not caught right now)
}
}
}
Example creating error boundary try-catch:
launch {
try {
// coroutineScope will wait for all the child co-routines to finish.
coroutineScope {
launch {
delay(1000)
throw RuntimeException("boom!")
}
}
} catch (e: CancellationException) {
// Re-throw CancellationException - do NOT suppress cancellation!
throw e
} catch (e: Exception) {
println("Caught: ${e.message}")
// Handle other exceptions - stops propagation
// Coroutine continues normally here
}
println("Launch continues after exception handling")
}
Details on individual Scope functions:
See Coroutine scope function and Error Boundary for detailed info on scope functions.
- Inherits a context from its parent, while creating a new Regular Job that inherits from parent Job.
- Uncaught Exception from one child:
- Waits for all its children before it can finish itself (when children do not throw)
- Cancels all its children when the parent is cancelled
// This is pseudo-code for purposes of understanding !!NOT TO BE USED AS IS!!
suspend fun <T> coroutineScope(block: suspend CoroutineScope.() -> T): T {
// Get the current coroutine context
val currentContext = coroutineContext
// Create a new Job that's a child of the current job
val scopeJob = Job(parent = currentContext[Job])
// Create a new scope with the new job
val scope = CoroutineScope(currentContext + scopeJob)
try {
// Execute the block and get result
val result = block(scope)
// This conceptually "waits" for all children to complete
// (In reality this would need to be done differently)
scopeJob.complete() // Signal no more children will be added
scopeJob.join() // Wait for existing children
return result
} catch (e: Exception) {
// Cancel all children on any exception
scopeJob.cancelChildren()
throw e
}
}
Another function that behaves a lot like coroutineScope is withTimeout. It also creates a scope and returns a value. Actually, withTimeout with a very big timeout behaves just like coroutineScope. The difference is that withTimeout additionally sets a time limit for its body execution. If it takes too long, it cancels this body and throws TimeoutCancellationException (a subtype of CancellationException).
Beware that withTimeout throws TimeoutCancellationException, which is a subtype of CancellationException (the same exception that is thrown when a coroutine is cancelled). So, when this exception is thrown in a coroutine builder, it only cancels it and does not affect its parent (as explained in the previous chapter). - Kotlin Coroutines Deep Dive
Relationships
Gotchas
The withContext function is similar to coroutineScope, but it additionally allows some changes to be made to the scope. The CoroutineContext provided as an argument to this function overrides the context from the parent scope (the same way as in coroutine builders). This means that withContext(EmptyCoroutineContext) and coroutineScope() behave in exactly the same way.
The function withContext is often used to set a different coroutine scope for part of our code. Usually, you should use it together with Dispatcher. - Kotlin Coroutines Deep Dive
Relationships
Gotchas
The supervisorScope function also behaves a lot like coroutineScope: it creates a CoroutineScope that inherits from the outer scope and calls the specified suspend block in it. The difference is that it overrides the context’s Job with Supervisor-Job, so it is not cancelled when a child raises an exception.
supervisorScope is mainly used in functions that start multiple independent tasks. - Kotlin Coroutines Deep Dive
Highlight
- Does NOT cancel when child raises an exception.
- Does NOT cancel children when child raises an exception.
- Waits for ALL children, even failing ones, even after some have failed and threw.
- Collects ALL exceptions from co-routines.
Notes
Notes
If you need to use functionalities from two coroutine scope func- tions, you need to use one inside another. For instance, to set both a timeout and a dispatcher, you can use withTimeoutOrNull inside withContext. - Kotlin Coroutines Deep Dive
suspend fun calculateAnswerOrNull(): User? =
withContext(Dispatchers.Default) {
withTimeoutOrNull(1000) {
calculateAnswer()
}
}
Children
Backlinks