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")}
// 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
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
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.
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