Skip to content
Coroutines: suspend, launch, async
step 1/6

Reading — step 1 of 6

Learn

~2 min readCoroutines and Delegation

Coroutines are Kotlin's flagship concurrency feature — covered in chapters 14-15 of Kotlin in Action and the entire kotlinx.coroutines reference. They let you write async code that LOOKS sequential.

suspend functions

A function marked suspend can pause and resume without blocking a thread:

import kotlinx.coroutines.*

suspend fun fetchUser(id: Int): String {
    delay(100)                    // suspends, doesn't block
    return "User \$id"
}

You can only call suspend functions from another suspend function or a coroutine builder (next section). The compiler enforces this — it transforms suspend functions into state machines under the hood.

Coroutine builders

fun main() = runBlocking {
    val user = fetchUser(42)
    println(user)
}
  • runBlocking { } — bridges blocking to coroutine code. Use for main or tests; not in real apps.
  • launch { } — fire-and-forget; returns a Job (cancellable handle).
  • async { } — like launch but returns a Deferred<T> with a result.
fun main() = runBlocking {
    // Parallel via async:
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    println("\${a.await()} and \${b.await()}")   // ~100ms total, not 200ms
}

CoroutineScope

Every coroutine runs in a SCOPE. The scope tracks all running coroutines and can cancel them en masse:

val scope = CoroutineScope(Dispatchers.Default)

val job = scope.launch {
    while (isActive) {
        // ... work ...
        delay(100)
    }
}

// Later:
job.cancel()           // cancels just this one
scope.cancel()         // cancels everything in the scope

Dispatchers — which thread

launch(Dispatchers.IO) { /* blocking I/O work */ }
launch(Dispatchers.Default) { /* CPU-bound work */ }
launch(Dispatchers.Main) { /* Android UI updates */ }

Dispatchers route coroutines to thread pools tuned for the workload type.

Cancellation is cooperative

val job = launch {
    repeat(1000) { i ->
        ensureActive()              // throws if cancelled
        // or check: if (!isActive) return@launch
        println(i)
        delay(10)
    }
}
delay(100)
job.cancel()

Most stdlib suspend functions check cancellation automatically. Long custom loops should call ensureActive() periodically.

Structured concurrency

launch and async are bound to their parent scope. The scope doesn't complete until all children do — and a failure in one child cancels siblings:

coroutineScope {
    launch { stepA() }
    launch { stepB() }
}   // both A and B must finish before this block returns

This prevents the classic concurrency leak where a failed task leaves orphans running.

Common mistakes

  • Using runBlocking in production app code — blocks the calling thread. Reserve for entry points and tests.
  • async without await — exceptions inside silently swallow. Always await or use launch.
  • Forgetting cancellation — long-running coroutines should call ensureActive() to honor cancel requests.
  • Using GlobalScope.launch — leaks. Use a structured scope with a clear lifecycle.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…