skip to content

How does runBlocking differ from launch and async as a coroutine builder?

level: middleimportance: must knowfreq 70%

answer

  1. runBlocking blocks; launch/async don't
  2. launch -> Job; async -> Deferred; runBlocking -> value
  3. launch/async need a CoroutineScope receiver
  4. runBlocking enters, launch/async structure
  5. Combine: runBlocking outside, async/launch inside

basics

~10 s

launch and async start coroutines that run alongside your code and return right away. runBlocking is a regular function that waits, blocking the thread until everything finishes, and gives back the result.

solid answer

~50 s

All three are coroutine builders, but they sit on opposite sides of the suspend boundary. `launch` and `async` are extension functions on `CoroutineScope` and are non-blocking: `launch` returns a `Job` (fire-and-forget), `async` returns a `Deferred<T>` you `await()` for a value, and both schedule work concurrently without blocking the caller. They can only be called where a scope exists (inside a coroutine, a suspend function with `coroutineScope`, or a scope you hold). `runBlocking`, by contrast, is a top-level regular (non-suspend) function: it creates a scope, runs the block, and **blocks the calling thread** until the block and all its children finish, returning the block's value. So launch/async are for structuring concurrency *within* the coroutine world; runBlocking is for *entering* that world from blocking code. You normally combine them: call runBlocking once at the boundary, then use launch/async inside.

code

kotlin · 8 lines
kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred: Deferred<Int> = async { 21 * 2 } // non-blocking, returns Deferred
    val job: Job = launch { delay(50) }            // non-blocking, returns Job
    job.join()                                     // suspend until launch done
    println(deferred.await())                      // 42
}

go deeper

for a junior

Knows launch/async are non-blocking and runBlocking blocks; can name the return types.

for a middle

Explains the scope-receiver requirement, return types, and the typical runBlocking-outside / async-launch-inside pattern.

for a senior

Connects all three to structured concurrency: child completion, exception and cancellation propagation through the Job tree.

for a principal

Sets conventions on where the single runBlocking boundary lives and how concurrency primitives compose in a service architecture.

## Three builders, two jobs A **coroutine builder** is a function that starts a coroutine. The key axis is *blocking vs non-blocking* and *where you can call it*. ### runBlocking — enter the coroutine world - A **regular (non-suspend) top-level function**. Callable from any ordinary code. - **Blocks the current thread** until the block and all child coroutines complete. - **Returns** the block's result (`T`). - Purpose: bridge from blocking code (main, tests) into suspend code. ### launch — fire-and-forget concurrency - An **extension on `CoroutineScope`**; callable only where a scope exists. - **Non-blocking**: returns a `Job` immediately and runs concurrently. - Returns no value; used for side effects. Exceptions propagate to the parent and (by default) cancel siblings. ### async — concurrent result - Also an **extension on `CoroutineScope`**, non-blocking. - Returns a `Deferred<T>`; call `.await()` (a suspend function) to get the value. - Used to run things in parallel and combine results. ## Putting them together ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { // ENTER coroutine world (blocks main thread) val a = async { fetch("A") } // start concurrently, returns Deferred val b = async { fetch("B") } // runs in parallel with a launch { log("started") } // fire-and-forget Job println(a.await() + b.await()) // suspend until both ready } suspend fun fetch(id: String): String { delay(100); return id } suspend fun log(msg: String) { delay(10); println(msg) } ``` `runBlocking` is called once at the boundary; inside it, `async` and `launch` structure the actual concurrency. Calling `launch`/`async` outside any scope won't compile because they need a `CoroutineScope` receiver. ## Comparison table (in prose) - **Blocking?** runBlocking yes; launch/async no. - **Receiver?** runBlocking none (top-level); launch/async need `CoroutineScope`. - **Returns?** runBlocking the block's `T`; launch a `Job`; async a `Deferred<T>`. - **Callable from non-suspend code?** runBlocking yes; launch/async no (need a scope). - **Typical use?** runBlocking = bridge/entry; launch = side-effect concurrency; async = parallel results. ## Structured concurrency All three respect **structured concurrency**: child coroutines started inside complete before the builder returns, and cancellation/exceptions propagate through the parent-child `Job` hierarchy.

  • Can you call launch directly in a plain non-suspend function with no scope?
    No — launch is an extension on CoroutineScope, so you need a scope (e.g. from runBlocking, coroutineScope, or a held CoroutineScope) first.
  • What does async give you that launch doesn't?
    A Deferred<T> result you can await(); launch returns a Job with no value.

saying these in an interview costs you the question

  • Saying launch/async block the caller like runBlocking
  • Claiming runBlocking returns a Job or Deferred
  • Not knowing launch/async require a CoroutineScope receiver
  • Confusing async's Deferred.await() with runBlocking's direct return
  • Thinking you must choose one builder rather than combining them

context