Coroutines Basics

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Coroutines are Kotlin’s answer to asynchronous programming — code that reads top-to-bottom like ordinary sequential code, but can suspend without blocking the underlying thread. They ship as a separate library, kotlinx.coroutines, not the language core itself, though the suspend keyword that makes them possible is a language feature.

suspend Functions

A function marked suspend may pause its execution at a well-defined point (e.g. while awaiting an I/O result) and resume later, without blocking the thread it started on — the compiler transforms it into a state-machine-like implementation behind the scenes:

suspend fun fetchUser(id: Int): User {
    delay(100)                     // "delay" suspends, unlike Thread.sleep which blocks the thread
    return httpClient.get("/users/$id").parseAsUser()
}

A suspend function can only be called from another suspend function, or from inside a coroutine — this "coloring" is deliberate: it makes it visible at every call site whether a function can suspend, the same way Kotlin makes nullability visible in a type rather than leaving it implicit.

Coroutine Builders: launch, async, runBlocking

A coroutine is started by a builder function, each suited to a different situation:

Builder Use

launch

fire-and-forget — starts a coroutine and returns a Job handle, with no result value.

async

starts a coroutine that does produce a result, returned as a Deferred<T>; call .await() to suspend until it completes.

runBlocking

bridges regular blocking code into the coroutine world by blocking the current thread until its coroutine body completes — almost always confined to a main function or a test, never used inside application code that already runs in a coroutine.

fun main() = runBlocking {                    // bridges "main" (not a suspend function) into coroutines
    launch {                                    // fire-and-forget child coroutine
        delay(100)
        println("launched work done")
    }

    val deferred = async {                      // produces a result
        delay(50)
        42
    }
    println("the answer is ${deferred.await()}") // suspends here until "async" completes

    println("main continues immediately after starting the coroutines above")
}

CoroutineScope

Every coroutine builder is an extension function on CoroutineScope — a coroutine can only be launched within a scope, and that scope is what ties the coroutine’s lifetime to something meaningful (a screen, a request, a test):

class UserRepository(private val scope: CoroutineScope) {
    fun refreshInBackground() {
        scope.launch {
            val user = fetchUser(1)
            println("refreshed: $user")
        }
    }
}

On Android, viewModelScope and lifecycleScope (see Kotlin for Android) are the ready-made scopes tied to a ViewModel’s or a `Lifecycle-aware component’s own lifetime, so coroutines are cancelled automatically when the screen goes away.

Structured Concurrency

Structured concurrency means a coroutine started inside a scope becomes a child of that scope, and the parent will not consider itself "done" until every child has finished — cancelling a parent automatically cancels every descendant, and an unhandled exception in a child propagates up to the parent by default. This is what prevents the classic "fire-and-forget coroutine that outlives its owner and leaks" bug a manually-managed thread pool is prone to:

suspend fun loadDashboard() = coroutineScope {   // creates a scope tied to THIS suspend call
    val user = async { fetchUser(1) }             // child 1
    val orders = async { fetchOrders(1) }          // child 2
    Dashboard(user.await(), orders.await())        // suspends until BOTH children complete
    // coroutineScope itself doesn't return until every child (including any it doesn't wait on
    // explicitly) has completed or been cancelled
}
flowchart TD P(["parent scope\n(e.g. viewModelScope)"]) --> C1["child: launch { fetchUser() }"] P --> C2["child: async { fetchOrders() }"] C2 --> C3["grandchild: launch { logAnalytics() }"] P -.->|"scope.cancel()\nor screen destroyed"| X1["child 1 cancelled"] P -.-> X2["child 2 cancelled"] P -.-> X3["grandchild cancelled too"] style X1 fill:#e05252,stroke:#8a1f1f,color:#fff style X2 fill:#e05252,stroke:#8a1f1f,color:#fff style X3 fill:#e05252,stroke:#8a1f1f,color:#fff

See Also