CoroutineScope

Creates a CoroutineScope with the given coroutine context.

The provided context should contain a Job, which will represent the lifecycle of the scope and become the parent of any coroutines launched in this scope.

If a Job is not passed, a new Job() is created and added to the context. This is error-prone and should be avoided; it is only provided for backward compatibility.

Intended usage

Non-lexically-scoped supervisorScope

The most common pattern of the CoroutineScope() builder function usage is to obtain a scope whose lifetime matches the lifetime of some object, with child coroutines performing operations on that object. Once the object gets destroyed, the coroutines in this scope must be cancelled. This is achieved with this pattern:

class ThingWithItsOwnLifetime(scope: CoroutineScope? = null): AutoCloseable {
private val scope = scope ?: CoroutineScope(
SupervisorJob() + Dispatchers.Main +
CoroutineExceptionHandler { _, e ->
// handle uncaught coroutine exceptions appropriately
}
)

fun doSomethingWithThing() = scope.launch {
// this computation gets cancelled when the thing gets destroyed
}

override fun close() {
// the computations should all stop running
scope.cancel()
}
}

The scope parameter is needed to support injecting coroutine scopes for testing.

Non-lexically-scoped coroutineScope

An equivalent to coroutineScope represents a group of tasks that work together to achieve one goal and should succeed or fail together.

class SubtaskPool(scope: CoroutineScope? = null) {
private val job = CompletableDeferred<Unit>()
private val scope = scope ?: CoroutineScope(job + Dispatchers.IO)

fun addPieceOfWork() = scope.launch {
// this coroutine will be cancelled when the pool is closed.
// if this coroutine fails, the pool will fail too
}

fun cancel() {
// this will cancel all the coroutines launched in this scope
job.cancel()
}

suspend fun await() {
job.complete(Unit)
job.await()
}
}

Pitfalls

No memoization

Every call to this function creates a new instance of CoroutineScope. If a Job instance is passed in the context, it is less efficient, but is not a bug, as then, the different CoroutineScope instances will still represent the same lifecycle and will be cancelled together. However, if the context does not contain a Job, then every created CoroutineScope will be unrelated to the previous ones.

// // 1) ANTIPATTERN! DO NOT WRITE: creates an independent scope every time
// val myScope: CoroutineScope get() = CoroutineScope(Dispatchers.Main)
// // 2) Write this instead:
// val myScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
myScope.launch {
try {
awaitCancellation()
} finally {
println("This line will only be printed in scenario 2)")
}
}
myScope.cancel()

Forgetting to propagate or handle exceptions

Every CoroutineScope without an explicit Job in its context is a potential source of crashes or degraded performance due to logging unhandled exceptions. CoroutineScope(Dispatchers.Default).launch { error("") } is enough to crash an Android or Kotlin/Native application. The reason for this is that the Job created for this scope does not have a parent, and therefore, there is no way to propagate the exception up the ancestry chain or to the caller of some function.

One way to avoid this is to use a CoroutineExceptionHandler in the context of the scope:

val scope = CoroutineScope(
Dispatchers.Default +
CoroutineExceptionHandler { _, e ->
// handle uncaught coroutine exceptions appropriately
}
)

scope.launch {
error("This is okay") // this will not crash the application
}

Another way is to provide a Job that knows how to propagate the exception. See CoroutineExceptionHandler for details of how exceptions are propagated. Here is an example of using a CompletableDeferred in the scope's context to properly handle exceptions:

val myDeferred = CompletableDeferred<Unit>()
try {
val scope = CoroutineScope(myDeferred + Dispatchers.Default)
scope.launch {
error("This is okay") // this will not crash the application
}
} finally {
// Do not forget to complete and await the deferred to check for exceptions!
myDeferred.complete(Unit)
// The exceptions from child coroutines will be thrown here:
myDeferred.await()
}

This specific example is not recommended, as it is easier to use coroutineScope to achieve a similar effect. However, it is more flexible, as it allows invoking the finally block separately.

Surprising interactions between failing children

If a Job is not passed in the context of this function explicitly, one is created using the Job() constructor function. As opposed to a SupervisorJob, this Job will fail if any of its children fail.

val scope = CoroutineScope(Dispatchers.Main + CoroutineExceptionHandler { _, e ->
println("Coroutine failed with exception $e")
})
scope.launch {
updateUI("Operation started!")
// this coroutine will be cancelled
// when the other coroutine fails
delay(2.seconds)
// this line will not be executed
updateUI("Operation finished!")
}
scope.launch {
error("This will cancel the other coroutines")
}

This behavior is suitable for cases where one task is decomposed into several subtasks, so one failure means the whole operation can no longer succeed and will have to be cancelled. However, in the far more common scenarios where CoroutineScope() represents the lifecycle of some entity that supports multiple independent concurrent operations, this is not the desired behavior.

Explicitly using a SupervisorJob in the context of the scope is the recommended way to avoid this.

Unintentionally passing a Job in the context

Sometimes, a Job is passed in the context of this function unintentionally, leading to unpredictable interactions between several scopes.

Examples of this include CoroutineScope(currentCoroutineContext()), CoroutineScope(coroutineContext), CoroutineScope(scope.coroutineContext), or any variations thereof that add new elements or remove existing ones.

In all of these cases, the Job passed in the argument will become the Job of the newly created CoroutineScope, meaning this scope will essentially be a view of some other scope, similar to what the CoroutineScope.plus operation produces.

// ANTIPATTERN! DO NOT WRITE SUCH CODE
suspend fun foo() {
val scope = CoroutineScope(currentCoroutineContext())
scope.launch {
delay(1.seconds)
println("foo()'s child coroutine finished")
}
}

suspend fun bar() {
coroutineScope {
foo()
println("foo() finished, but the new coroutine is still running")
}
}

In this example, the new coroutine will be launched in the scope of the caller, and foo() will not await the completion of this coroutine. To await the completion, use coroutineScope instead:

suspend fun foo() = coroutineScope {
launch {
delay(1.seconds)
println("foo()'s child coroutine finished")
}
} // `foo()` will only return when the child coroutine completes

suspend fun bar() {
coroutineScope {
foo()
println("foo() finished, along with its child coroutine")
}
}

If launching a coroutine in the context of the caller is the desired behavior, make it explicit by passing the outer scope as a parameter:

fun foo(scope: CoroutineScope) {
scope.launch {
delay(1.seconds)
println("foo()'s child coroutine finished")
}
}

suspend fun bar() {
coroutineScope {
foo(this)
println("foo() created a child coroutine in `this` scope")
}
}