launch and async
Kotlin coroutines give you two main tools for starting concurrent work: launch and async. Both start a new coroutine that runs concurrently with the code around it, but they hand back different things and are meant for different jobs. Getting this distinction right is the single most important step in writing coroutine code that is both correct and actually concurrent, rather than code that merely looks concurrent while running sequentially by accident.
Overview: How launch and async Work
Both launch and async are coroutine builders — functions that create and immediately start a new coroutine on a CoroutineScope. They cannot be called from arbitrary code; they need a scope, which is why almost every example wraps things in runBlocking (a builder that blocks the current thread until its coroutine, and all of its children, finish — useful for main functions and tests, but rarely used in real application code, where you get a scope from a framework like Android’s viewModelScope).
launch is for fire-and-forget work: you want the code to run, but you don’t need a value back. It returns a Job, a handle you can use to cancel the coroutine or wait for it to finish (with job.join()), but a Job carries no result.
async is for work that produces a value. It returns a Deferred<T>, which is a Job that also holds a future result of type T. You retrieve that result by calling the suspending function await(), which suspends the caller until the coroutine completes and then returns its value (or rethrows its exception, if it failed).
The key thing to understand about how this works under the hood is that neither builder blocks a thread while it waits. A suspending function like delay() or await() saves its continuation (essentially, “where to resume and with what local state”) and gives the underlying thread back to the coroutine dispatcher, which can run other coroutines on it. When the awaited work finishes, the dispatcher resumes the continuation, possibly on the same thread, possibly on another one from its pool. This is fundamentally different from a blocked OS thread, which sits idle holding its stack and cannot do anything else until it is unblocked. That’s why a single thread can juggle thousands of coroutines but only a handful of blocked threads.
Both builders also participate in structured concurrency: a coroutine started with launch or async becomes a child of the scope it was launched in. The parent scope (for example, runBlocking) will not complete until all of its children complete, and if a child fails with an exception, that failure propagates up and cancels its siblings by default. This is why you never see “leaked” background coroutines in well-structured Kotlin code — the compiler and runtime enforce that every coroutine has an owner.
Syntax
The general shape of each builder:
// Fire-and-forget: returns a Job, no result value
val job = launch {
// suspendable code here
}
// Produces a value: returns a Deferred<T>
val deferred = async {
// suspendable code here, last expression is the result
42
}
val result: Int = deferred.await()
| Builder | Returns | Use when… |
|---|---|---|
launch |
Job |
You want a side effect (write to a file, update UI, log something) and don’t need a return value. |
async |
Deferred<T> |
You need to compute a value concurrently with other work, then combine the results with await(). |
Examples
Example 1: basic launch. This example shows that launch starts a coroutine and returns control immediately — the code after launch keeps running without waiting for it.
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start: ${Thread.currentThread().name}")
launch {
delay(1000L)
println("Coroutine finished: ${Thread.currentThread().name}")
}
println("End of main coroutine body")
}
Output:
Start: main
End of main coroutine body
Coroutine finished: main
“End of main coroutine body” prints before “Coroutine finished” because launch doesn’t wait for its body — it schedules the coroutine and returns immediately. runBlocking, however, will not let main exit until that launched child completes, which is why the final line still appears before the program ends.
Example 2: async and await. Here two computations run concurrently and their results are combined once both are ready.
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferredA: Deferred<Int> = async {
delay(500L)
10
}
val deferredB: Deferred<Int> = async {
delay(300L)
20
}
val sum = deferredA.await() + deferredB.await()
println("Sum: $sum")
}
Output:
Sum: 30
Both async blocks start right away, so the 500ms and 300ms delays overlap — the whole program takes about 500ms, not 800ms. Calling await() on deferredA first only suspends until that value is ready; by the time it resumes, deferredB‘s 300ms wait has usually already finished in the background.
Example 3: a more realistic case — fetching several items concurrently.
import kotlinx.coroutines.*
data class UserProfile(val id: Int, val name: String)
suspend fun fetchProfile(id: Int): UserProfile {
delay(200L)
return UserProfile(id, "User$id")
}
fun main() = runBlocking {
val ids = listOf(1, 2, 3)
val deferredProfiles = ids.map { id -> async { fetchProfile(id) } }
val profiles = deferredProfiles.awaitAll()
profiles.forEach { println(it) }
}
Output:
UserProfile(id=1, name=User1)
UserProfile(id=2, name=User2)
UserProfile(id=3, name=User3)
Each id kicks off its own async coroutine, so all three simulated network calls overlap and the whole batch takes roughly 200ms instead of 600ms sequential. UserProfile is a data class, so its auto-generated toString() is what produces the readable UserProfile(id=1, name=User1) output above.
How It Works Step by Step
Walking through Example 2 in order:
runBlockingstarts a coroutine and blocks the calling thread until that coroutine (and all its children) finish.async { ... }fordeferredAcreates a child coroutine and schedules it; execution of therunBlockingbody continues immediately, it does not wait.async { ... }fordeferredBdoes the same — now two child coroutines are both pending, effectively running side by side.deferredA.await()suspends therunBlockingcoroutine untildeferredA‘s body completes and produces10. While suspended, the thread is free, sodeferredB‘s coroutine can keep progressing on it.deferredB.await()suspends again ifdeferredBisn’t done yet, or returns immediately with20if it already finished during the first suspension.- The sum is computed and printed, then
runBlockingreturns because it has no more pending children.
Common Mistakes
Mistake 1: awaiting immediately after each async, which cancels the concurrency.
import kotlinx.coroutines.*
suspend fun fetchA(): Int { delay(300L); return 1 }
suspend fun fetchB(): Int { delay(300L); return 2 }
fun main() = runBlocking {
val a = async { fetchA() }.await() // suspends here...
val b = async { fetchB() }.await() // ...before b even starts
println(a + b)
}
Calling .await() right after each async forces the program to wait for fetchA to fully finish before fetchB even starts, so the two 300ms delays run one after another (~600ms total). This defeats the entire point of using async — it behaves no better than sequential code, just with extra overhead.
import kotlinx.coroutines.*
suspend fun fetchA(): Int { delay(300L); return 1 }
suspend fun fetchB(): Int { delay(300L); return 2 }
fun main() = runBlocking {
val deferredA = async { fetchA() } // starts immediately
val deferredB = async { fetchB() } // also starts immediately
println(deferredA.await() + deferredB.await()) // now wait for both
}
Starting both coroutines first, and only awaiting afterward, lets the two delays overlap so the total time is about 300ms.
Mistake 2: calling a blocking builder like runBlocking from inside suspending code.
import kotlinx.coroutines.*
suspend fun loadData(): String {
return runBlocking { // wrong: blocks the underlying thread
delay(100L)
"data"
}
}
fun main() = runBlocking {
println(loadData())
}
loadData is already a suspend function, so nesting runBlocking inside it is unnecessary and harmful: runBlocking parks the real thread until its block finishes, which can starve the dispatcher’s thread pool and even deadlock if that thread is needed to resume other coroutines. Just use delay directly, since the enclosing function is already suspending.
import kotlinx.coroutines.*
suspend fun loadData(): String {
delay(100L)
return "data"
}
fun main() = runBlocking {
println(loadData())
}
Best Practices
- Use
launchfor side-effecting work where you don’t need a result; useasynconly when you actually need the value it produces. - Start every
asyncyou plan to combine before callingawait()on any of them, so their work overlaps. - Prefer
awaitAll()over a list of individual.await()calls in a loop — it reads clearer and fails fast if any deferred throws. - Never call a blocking builder such as
runBlockingfrom inside asuspendfunction or another coroutine — only use it at the true entry point (amainfunction or a test). - Let structured concurrency work for you: launch coroutines in a scope tied to the lifetime that owns them (a request, a screen, a test) instead of a global, unscoped launcher, so failures and cancellation propagate correctly.
- Reserve
Deferredfor values you actually intend toawait; a fire-and-forgetasyncwhose result is discarded should be alaunchinstead, since an un-awaited exception can behave differently from an un-joined one.
Practice Exercises
- Write a program with three
asynccoroutines that eachdelaya different amount of time and return anInt. Sum all three withawaitAll()and print the total. - Take the “sequential await” mistake shown above with two 400ms delays and time how long the wrong version takes versus the fixed version (you can print
System.currentTimeMillis()before and after) — confirm the fixed version is roughly half the time. - Write a function that uses
launchto log “Task started” and “Task finished” around adelay, and explain in a comment why you couldn’t use the launched coroutine’s return value even if you wanted to.
Summary
launchstarts a coroutine and returns aJob; use it for fire-and-forget work with no result.asyncstarts a coroutine and returns aDeferred<T>; callawait()to suspend until its value is ready.- Suspending functions like
delayandawaitfree the underlying thread instead of blocking it, which is why coroutines scale far better than raw threads. - Start all the
asynccalls you need before awaiting any of them, or you accidentally turn concurrent code back into sequential code. - Coroutines follow structured concurrency: a parent scope waits for all its children and propagates their failures, so avoid mixing in blocking calls like nested
runBlocking.
