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 give you any value back.
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 a
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()returns .
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
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()
)
}
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()
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" |
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
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 ?
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.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.
Android Foreground Service Notification Channels in Kotlin
Learn why startForeground fails on Android 8.1 and how to create a valid notification channel for foreground services in Kotlin.