CoroutineScope — what it is, lifecycle and workings in coroutines

Author: IT Sectr Published: 2026-06-22 Reading time: 10 min

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 — an interface with a single CoroutineContext field, defining the coroutine lifecycle
  • Job — a context element responsible for cancellation: cancelling the scope cancels all child coroutines
  • Structured concurrency — the principle where child coroutines are bound to the parent scope
  • GlobalScope — an application-wide scope that is not recommended due to memory leak risks
  • supervisorScope — a special scope where cancelling one child coroutine does not cancel the others

What is CoroutineScope in Kotlin?

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.

kotlin
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.

Role in the kotlinx.coroutines library

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.

Where CoroutineScope is used

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.

How CoroutineScope works: Job and structured concurrency

Understanding the inner workings of CoroutineScope requires familiarity with the concept of Job and the principle of structured concurrency.

Job — the coroutine task

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:

  • Parent Job — the scope in which the coroutine was launched
  • Child Job — each coroutine launched via launch/async
  • Parent cancellation → cancellation of all children
  • Exception in a child → parent cancellation (except in supervisorScope)

The principle of structured concurrency

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:

  • Predictable lifecycle — when the scope completes, all coroutines are guaranteed to be stopped
  • Automatic error handling — an exception in any child coroutine propagates to the scope
  • No memory leaks — no coroutine remains running after the scope completes
  • Clear hierarchy — the code reflects the logical structure of parallel operations

CoroutineScope lifecycle

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.

Creating and configuring CoroutineScope

You can create a CoroutineScope via a factory function or by implementing the interface in your class. Let us explore both approaches.

Factory function CoroutineScope()

kotlin
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.

Implementing the interface via composition

kotlin
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.

Implementation via delegation

Kotlin allows delegating CoroutineScope implementation through the by keyword:

kotlin
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 vs custom CoroutineScope

GlobalScope is a singleton CoroutineScope for the entire application. Its use in production code is officially not recommended.

Problems with GlobalScope

  • Lack of structured concurrency — coroutines in GlobalScope are not tied to the component lifecycle
  • Memory leaks — a coroutine may continue running after the Activity/Fragment is closed
  • Difficult testing — GlobalScope cannot be replaced in tests
  • Uncontrolled resource consumption — many coroutines may run longer than expected

When GlobalScope is justified

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()).

Recommendation

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.

coroutineScope vs supervisorScope: what is the difference

Both functions are suspend functions that create a temporary scope for parallel tasks, but their behavior with exceptions differs fundamentally.

CharacteristiccoroutineScopesupervisorScope
Behavior on errorAn exception in a child coroutine cancels all othersAn exception in a child coroutine does NOT cancel the others
Error propagationYes, the first exception is propagated outwardYes, the first exception is propagated outward
Default JobJob() — children are tied to the parentSupervisorJob() — children do not depend on each other
Typical use caseAtomic multi-step operationIndependent parallel tasks (UI loads)

When to choose coroutineScope

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.

kotlin
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.

When to choose supervisorScope

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.

Common mistakes when working with CoroutineScope

Let us look at the most common developer mistakes when using CoroutineScope in Kotlin.

Mistake 1: Forgot to cancel the scope

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.

Mistake 2: Using GlobalScope in Activity or Fragment

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.

Mistake 3: Reusing a cancelled scope

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.

Mistake 4: Incorrect CoroutineScope interface delegation

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

How is CoroutineScope different from CoroutineContext?

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.

Can I create a CoroutineScope with SupervisorJob?

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.

How many coroutines can a CoroutineScope contain?

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.

How to test code with CoroutineScope?

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.

Can a coroutine have its own scope?

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

  • CoroutineScope — an interface with a coroutineContext field, defining the lifecycle of coroutines launched within it
  • Structured concurrency — cancelling the scope automatically cancels all child coroutines, preventing memory leaks
  • Job and SupervisorJob — two error handling modes: cascading cancellation (Job) and isolated errors (SupervisorJob)
  • GlobalScope — not recommended for production due to the lack of lifecycle binding
  • coroutineScope vs supervisorScope — atomic parallel operations vs independent parallel tasks
  • viewModelScope and lifecycleScope — ready-made scopes for Android, automatically cancelled when the component finishes
  • Factory function — the preferred way to create a scope via CoroutineContext + explicit cancel call

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.

Discuss the project

Read also