Question
In the kotlinx.coroutines library, you can start a new coroutine using either launch with join(), or async with await(). What is the difference between these approaches, and when should each one be used?
Short Answer
By the end of this page, you will understand how launch/join and async/await differ in Kotlin coroutines. You will learn that launch is mainly used for work that does not return a value, while async is used for concurrent work that produces a result. You will also see how join() and await() behave, how exceptions differ, and how these patterns are used in real Kotlin code.
Concept
launch and async both start coroutines, but they are designed for different goals.
launchstarts a coroutine that returns aJobasyncstarts a coroutine that returns aDeferred<T>
A Job represents a coroutine whose main purpose is to do some work.
A Deferred<T> is a Job that will eventually produce a value of type T.
That is the core difference:
- Use
launchwhen you want to start work and do not need a result - Use
asyncwhen you want to start work and later get a result
join() is used with a Job:
val job = launch {
println("Working...")
}
job.join()
join() waits until the coroutine finishes, but it does not give you any value back.
await() is used with a Deferred<T>:
val deferred = async {
42
}
val result = deferred.await()
await() waits until the coroutine finishes and returns its result.
Why this matters
In real programs, some tasks are just actions:
- save a log entry
- send analytics
- update UI state
- write to a file
These usually fit launch.
Other tasks compute or fetch something:
- load a user from an API
- calculate a total price
- read configuration data
- combine multiple database results
These usually fit async.
Exception behavior
Another important difference is how they communicate failure.
- A coroutine started with
launchthrows exceptions immediately to its parent - A coroutine started with
asyncstores exceptions untilawait()is called
This means async is meant for deferred results, including deferred failures.
In structured concurrency, both still belong to their parent scope, so failures still matter. But the API design reflects their intent:
launch= fire off a taskasync= start a task that you plan to collect later
Mental Model
Think of launch and async like assigning work to people.
launchis like telling someone: "Please do this task." Later, you can ask: "Are you finished?" That isjoin().asyncis like telling someone: "Please do this task and bring me the answer." Later, you ask: "What result did you get?" That isawait().
So:
launch= work without a return valuejoin= wait for completionasync= work with a future resultawait= wait for completion and receive the value
A useful shortcut is:
Job= "I am doing work"Deferred<T>= "I am doing work and will give you aTlater"
Syntax and Examples
Basic syntax
launch with join
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("Task finished")
}
println("Waiting...")
job.join()
println("Done")
}
async with await
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(1000)
42
}
println("Waiting...")
val result = deferred.await()
println("Result: $result")
}
What these examples show
In the first example:
launchreturns aJob- the coroutine prints a message
join()waits for it to finish- no value is returned
In the second example:
asyncreturns aDeferred<Int>- the coroutine computes
42 await()waits for it and returns the integer
A common real use of async
async is especially useful when you want multiple results at the same time.
import kotlinx.coroutines.*
suspend fun getUserName(): String {
delay(1000)
return "Ava"
}
suspend fun getUserScore(): Int {
delay(1000)
return 95
}
fun main() = runBlocking {
val nameDeferred = async { getUserName() }
val scoreDeferred = async { getUserScore() }
val name = nameDeferred.await()
val score = scoreDeferred.await()
println("$name has score $score")
}
Because both coroutines start before either await(), the total time is about 1 second, not 2 seconds.
That is one of the biggest reasons to use async.
Step by Step Execution
Consider this example:
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(500)
10 + 20
}
println("Before await")
val result = deferred.await()
println("Result: $result")
}
Step by step
runBlockingstarts a coroutine scope and blocks the main thread until all work inside completes.async { ... }starts a new coroutine immediately.- That coroutine pauses for
500milliseconds because ofdelay(500). - Meanwhile, the next line runs:
println("Before await"). - The program reaches
deferred.await(). - If the async coroutine has not finished yet,
await()suspends until it does. - After the delay, the async coroutine computes
10 + 20, producing30. await()returns30.- The final line prints
Result: 30.
Equivalent launch example
import kotlinx.coroutines.*
fun main() = runBlocking {
var result = 0
val job = launch {
delay(500)
result = 10 + 20
}
println("Before join")
job.join()
println("Result: $result")
}
This works, but it is less clean because:
- you need shared mutable state (
var result) launchis not designed to return valuesasyncexpresses the intent more clearly
Real World Use Cases
When launch is a good fit
Use launch for tasks that mainly perform side effects:
- writing logs
- sending analytics events
- updating local cache
- showing or hiding UI loading states
- triggering background cleanup work
Example:
scope.launch {
analytics.logEvent("button_clicked")
}
When async is a good fit
Use async when you need a value back:
- fetching data from two APIs in parallel
- reading multiple files concurrently
- calculating independent values and combining them
- loading related database records at the same time
Example:
coroutineScope {
val userDeferred = async { api.fetchUser() }
val postsDeferred = async { api.fetchPosts() }
val user = userDeferred.await()
val posts = postsDeferred.await()
UserScreenData(user, posts)
}
A practical guideline
Ask this question:
Do I need a result from this coroutine?
- No ->
launch - Yes ->
async
Real Codebase Usage
In real Kotlin projects, developers usually choose between these based on intent and readability.
Common launch patterns
Event handling
viewModelScope.launch {
repository.refresh()
}
Used when a button click or lifecycle event should start work.
Guard clauses inside a coroutine
viewModelScope.launch {
val token = sessionManager.token ?: return@launch
api.sendHeartbeat(token)
}
This is common when some condition must exist before doing work.
Error handling for side effects
viewModelScope.launch {
try {
repository.saveSettings()
} catch (e: Exception) {
logger.error(e)
}
}
Common async patterns
Parallel data loading
suspend fun loadDashboard(): DashboardData = coroutineScope {
val stats = async { repository.loadStats() }
val messages = async { repository.loadMessages() }
DashboardData(
stats = stats.await(),
messages = messages.await()
)
}
Avoiding sequential delays
Bad:
val user = repository.loadUser()
val orders = repository.loadOrders()
If these are independent, they may be better as:
coroutineScope {
val user = async { repository.loadUser() }
val orders = async { repository.loadOrders() }
combine(user.await(), orders.await())
}
Important codebase habit
In real projects, async is usually used inside coroutineScope or another structured scope, not as untracked background work. That keeps cancellation and errors predictable.
Common Mistakes
1. Using async when no value is needed
Broken idea:
scope.async {
println("Saved")
}
If you do not need a result, this should usually be:
scope.launch {
println("Saved")
}
Why: async communicates that a value will be awaited later.
2. Forgetting to call await()
val deferred = async { 42 }
println(deferred) // prints Deferred object, not 42
Correct:
val deferred = async { 42 }
println(deferred.await())
3. Using launch to return a value through shared mutable state
Less ideal:
var data = ""
val job = launch {
data = fetchData()
}
job.join()
println(data)
Better:
val deferred = async {
fetchData()
}
println(deferred.await())
4. Accidentally making work sequential
This is sequential, not parallel:
val a = async { loadA() }.await()
val b = async { loadB() }.await()
Why: you wait for a before even starting to wait meaningfully for both results together.
Better:
val a = async { loadA() }
val b = async { loadB() }
println(a.await() + b.await())
5. Ignoring exceptions from async
val deferred = async {
error("Something failed")
}
If you never call await(), you may never observe the result in the intended place.
Correct pattern:
try {
deferred.await()
} catch (e: Exception) {
println("Caught: ${e.message}")
}
6. Thinking join() and await() are interchangeable
They both wait, but:
join()only waits for completionawait()waits and returns the value
Use the one that matches the coroutine type and intent.
Comparisons
| Feature | launch | async |
|---|---|---|
| Returns | Job | Deferred<T> |
| Produces a value | No | Yes |
| Waiting method | join() | await() |
| Best for | Side effects, actions | Concurrent computations, fetched results |
| Exception style | Propagates as coroutine failure | Stored until await() |
| Typical intent | "Do this work" | "Compute this result" |
join() vs await()
| Method | Used with | What it does |
|---|---|---|
join() | Job | Waits until coroutine finishes |
await() | Deferred<T> | Waits until coroutine finishes and returns T |
launch/join vs async/await
- Choose
launch/joinfor completion tracking - Choose
async/awaitfor result gathering
If your code needs to combine values from multiple coroutines, async is usually the better choice.
If your code just needs to make sure some task has finished, launch with join() is enough.
Cheat Sheet
val job: Job = launch {
// work with no returned value
}
job.join()
val deferred: Deferred<Int> = async {
42
}
val result: Int = deferred.await()
Rules to remember
launchreturnsJobasyncreturnsDeferred<T>Deferred<T>is a kind ofJobjoin()waits for completion onlyawait()waits and gives back the result- Use
launchfor side effects - Use
asyncfor values
Good default choices
- Logging, saving, notifying ->
launch - Fetching, computing, combining results ->
async
Common edge cases
- If you never need a result, do not use
async - If you use
async, remember toawait() - Start multiple
asynctasks before awaiting if you want concurrency - Prefer structured scopes like
coroutineScopeorrunBlocking
Quick memory trick
launch= launch an actionasync= asynchronous result later
FAQ
What is the main difference between launch and async in Kotlin?
launch is for starting a coroutine that does not return a value. async is for starting a coroutine that returns a value later.
Does join() return the result of a coroutine?
No. join() only waits for the coroutine to finish. It does not return any value.
Does await() wait like join()?
Yes. await() waits for completion, but it also returns the computed result.
When should I use async instead of launch?
Use async when you need the coroutine to produce a value, especially if you want to run several independent computations concurrently.
Can I use launch to compute a value?
You can, but it usually leads to shared mutable state and less clear code. async is the better tool for returning values.
Is Deferred the same as Job?
Deferred<T> is similar to Job, but it also holds a future result of type T.
Why use async if I could just call a suspend function directly?
A direct suspend call is often sequential. async is useful when you want multiple independent suspend operations to start before waiting for the results.
Can async and launch both run concurrently?
Yes. Both create coroutines. The difference is mainly whether you expect a returned value.
Mini Project
Description
Build a small Kotlin coroutine program that loads two independent pieces of data: a user name and a user score. The project demonstrates when async is better than launch because both tasks return values that must be combined into one final message.
Goal
Create a coroutine-based program that fetches two values concurrently and prints a combined result.
Requirements
- Create one suspend function that returns a user name after a delay.
- Create another suspend function that returns a user score after a delay.
- Start both operations concurrently inside a coroutine scope.
- Wait for both results and print them in one sentence.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.