CoroutineScope is a Kotlin interface that defines the lifecycle of a coroutine and provides a context for launching new coroutines. According to the Kotlin documentation, 2025, each CoroutineScope instance contains a CoroutineContext and manages all coroutines launched within it. When the scope is cancelled, all child coroutines are automatically cancelled, preventing memory leaks.
Key takeaways
CoroutineScope is a fundamental interface from the kotlinx.coroutines library that serves as a container for coroutines. It defines the boundaries of coroutine lifecycle: when the scope completes, all coroutines inside it are automatically cancelled.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
The interface contains just one field — coroutineContext. Through it, the scope provides a dispatcher (Dispatcher), a job (Job), an exception handler, and other context elements for all coroutines launched within it.
All coroutine launching functions — launch, async, runBlocking — are extension functions on CoroutineScope. This means they can only be called when a scope object is available. This design guarantees that every coroutine has a clearly defined parent and lifecycle.
In Android, every architectural component has its own scope: viewModelScope for ViewModel, lifecycleScope for Activity/Fragment. In server applications, a scope can be tied to an HTTP request or a database connection pool.
Understanding the inner workings of CoroutineScope requires familiarity with the concept of Job and the principle of structured concurrency.
Each coroutine upon launch returns a Job object (or Deferred for async). Job represents a task with a finite lifecycle: New, Active, Completing, Completed, Cancelling, Cancelled. Job objects form a tree structure:
Structured concurrency is a key architectural principle of Kotlin Coroutines, where the coroutine lifecycle is tied to the lifecycle of its scope. This contrasts with the “fire-and-forget” model, where a coroutine continues to live after the scope completes. Advantages of structured concurrency:
When scope.cancel() is called, the scope's Job transitions to the Cancelled state, which recursively cancels all child Jobs. After cancellation, the scope can only be reused if a new CoroutineScope instance is created.
You can create a CoroutineScope via a factory function or by implementing the interface in your class. Let us explore both approaches.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
The factory function takes a CoroutineContext and creates a scope with the specified context. The example uses Dispatchers.Default for CPU-intensive tasks and SupervisorJob, which isolates exceptions between child coroutines.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
We store the scope as a class field and manually call cleanup to cancel it. This approach suits components with a managed lifecycle — for example, repositories or managers.
Kotlin allows delegating CoroutineScope implementation through the by keyword:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
This approach is convenient when the class itself is a scope and wants to provide coroutine launching methods. However, be careful: the class inherits all CoroutineScope methods, including cancel, which may break encapsulation.
GlobalScope is a singleton CoroutineScope for the entire application. Its use in production code is officially not recommended.
JetBrains allows GlobalScope only in rare scenarios: application-level background processes that should live even after all Activities are closed (e.g., data synchronization, analytics). But even in these cases, it is preferable to create your own scope with CoroutineScope(SupervisorJob()).
Always use a custom CoroutineScope with explicit lifecycle management. In Android, these are viewModelScope and lifecycleScope. In server applications, create a scope for each request or connection pool.
Both functions are suspend functions that create a temporary scope for parallel tasks, but their behavior with exceptions differs fundamentally.
| Characteristic | coroutineScope | supervisorScope |
|---|---|---|
| Behavior on error | An exception in a child coroutine cancels all others | An exception in a child coroutine does NOT cancel the others |
| Error propagation | Yes, the first exception is propagated outward | Yes, the first exception is propagated outward |
| Default Job | Job() — children are tied to the parent | SupervisorJob() — children do not depend on each other |
| Typical use case | Atomic multi-step operation | Independent parallel tasks (UI loads) |
Use coroutineScope when multiple parallel operations form a single atomic operation. For example, loading data from three servers: if one request fails, the others are meaningless.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
If getProduct or getReviews throws an exception — both coroutines are cancelled, and the exception is propagated to the calling code.
Use supervisorScope when parallel operations do not depend on each other. For example, loading profile data in several independent sections: if the recommendations section fails, the profile header and friends list should still be displayed.
Let us look at the most common developer mistakes when using CoroutineScope in Kotlin.
The most common scenario for coroutine leakage is creating a scope without calling cancel when the component finishes. If the scope is not cancelled, coroutines continue running, holding references to objects. In Android, use viewModelScope or lifecycleScope, which are cancelled automatically.
GlobalScope ignores the Android component lifecycle. A coroutine launched in GlobalScope after an Activity is closed will continue executing and attempt to update the UI — leading to a crash. Always use lifecycleScope for UI components.
After calling cancel(), the scope cannot be reused — all coroutines within it are already completed. Create a new CoroutineScope instance via the factory function. Job() does not support reactivation.
When delegating with by, the class gets a public cancel() method that can be called from anywhere, breaking encapsulation. Store the scope as a private field instead of delegating the interface.
Frequently asked questions
CoroutineScope is an interface that owns a CoroutineContext and is responsible for the coroutine lifecycle. CoroutineContext is a set of elements (dispatcher, job, error handler) that defines “how” a coroutine executes. One difference: the scope creates coroutines, while the context controls their behavior.
Yes, it is a standard pattern: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob prevents cascading cancellation of child coroutines when one of them throws an exception. This is useful for independent parallel tasks where an error in one should not stop the others.
There is no limit on the number of coroutines in a scope — they are only limited by available memory and dispatcher settings. The practical limit is typically thousands of active coroutines within a single scope. However, a large number of coroutines may indicate architectural problems.
The proper way is to pass the scope to the class via constructor or use runBlockingTest / runTest from kotlinx-coroutines-test. In tests, you can replace the scope with TestCoroutineDispatcher and manually control coroutine execution.
No, a scope is an external container for a coroutine. The coroutine itself is not a scope. However, inside a coroutine you can create a new scope via coroutineScope or supervisorScope for launching child coroutines in parallel.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also