Coroutine Builders — launch, async, runBlocking

Introduction

In Kotlin, coroutine builders are functions that start new coroutines. The main builders are launch, async, and runBlocking. Each serves different purposes, return different types, and behave differently in terms of cancellation and threading.

launch

What it does: Starts a coroutine for side-effects (fire-and-forget). Returns a Job to manage it.

val job = CoroutineScope(Dispatchers.Default).launch {
    repeat(5) { i ->
        if (!isActive) return@launch  // cooperative cancellation
        println("Working \$i on \${Thread.currentThread().name}")
        delay(300)
    }
}

// Cancel after 800ms
CoroutineScope(Dispatchers.Default).launch {
    delay(800)
    job.cancel()
    println("Job cancelled")
}

Expected output:


Working 0 on DefaultDispatcher-worker-1
Working 1 on DefaultDispatcher-worker-1
Working 2 on DefaultDispatcher-worker-1
Job cancelled

async

What it does: Starts a coroutine for computations and returns a Deferred<T> for the result. Use await() to get the value.

runBlocking {
    val d1 = async(Dispatchers.Default) {
        delay(1000)
        "Result 1"
    }
    val d2 = async(Dispatchers.Default) {
        delay(1200)
        "Result 2"
    }

    println("Waiting for results...")
    val r1 = d1.await()
    val r2 = d2.await()
    println("Results: \$r1, \$r2")
}

Expected output:


Waiting for results...
Results: Result 1, Result 2

runBlocking

What it does: Blocks the current thread until the coroutine completes. Useful in main() or tests.

fun main() = runBlocking {
    launch {
        delay(500)
        println("Inside runBlocking")
    }
    println("runBlocking will wait for children before returning")
}

Expected output:


runBlocking will wait for children before returning
Inside runBlocking

Cancellation & Exception Handling

Example 1: Basic Cancellation


import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(5) { i ->
            println("Working $i ...")
            delay(300)
        }
    }

    delay(700)
    println("Cancelling job!")
    job.cancel()
    job.join() // waits for cancellation to complete
    println("Job cancelled!")
}
  

Explanation: cancel() sends a cancellation signal, and join() suspends until the coroutine actually stops. Always use both for proper cleanup.

Example 2: Cooperative Cancellation


import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        var i = 0
        while (isActive) { // cooperative cancellation check
            println("Processing item ${'$'}i")
            i++
            delay(200)
        }
    }
    delay(600)
    job.cancelAndJoin()
    println("Gracefully stopped.")
}
  

Explanation: If you don’t use isActive, CPU-bound loops won’t respond to cancel() immediately.

Example 3: Exception Handling


import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, ex ->
        println("Caught exception: ${'$'}{ex.message}")
    }

    val scope = CoroutineScope(SupervisorJob() + handler)

    with(scope) {
        val job1 = launch {
            println("Failing child")
            throw RuntimeException("Something went wrong!")
        }

        val job2 = launch {
            delay(500)
            println("Sibling coroutine still running")
        }

        job1.join()
        job2.join()
    }
}
  

Explanation: Using SupervisorJob, even if one child fails, the other continues. Without it, both coroutines would be cancelled.

Start Modes

Example 4: Start Modes Demo


import kotlinx.coroutines.*

fun main() = runBlocking {
    val lazyJob = launch(start = CoroutineStart.LAZY) {
        println("Lazy job started in ${'$'}{Thread.currentThread().name}")
    }

    println("Job not started yet")
    delay(300)
    lazyJob.start() // or lazyJob.join()
    println("Main finished")
}
  

Explanation: The coroutine with CoroutineStart.LAZY won't start until explicitly requested.

Comparison Table

Builder Returns Use-case Cancellation / Exception Behavior
launch Job Fire-and-forget tasks (e.g., network calls, saving data) Exceptions cancel parent unless caught with CoroutineExceptionHandler
async Deferred<T> Concurrent computations returning results Exceptions rethrown on await(); cancels parent unless supervisor used
runBlocking T Blocking bridge for main functions and tests Blocks the current thread until all child coroutines complete

Example 5: launch vs async


import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch
    val job = launch {
        delay(500)
        println("launch finished")
    }

    // async
    val deferred = async {
        delay(500)
        "Result from async"
    }

    println("Async returned: ${'$'}{deferred.await()}")
    job.join()
    println("All done!")
}
  

Explanation: launch returns a Job (no result), while async returns a Deferred which can produce a result using await().