Question

launch/join vs async/await in Kotlin Coroutines

kotlinasynchronouscoroutinekotlin-coroutines

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.

  • launch starts a coroutine that returns a Job
  • async starts a coroutine that returns a Deferred<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 launch when you want to start work and do not need a result
  • Use async when 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 launch throws exceptions immediately to its parent
  • A coroutine started with async stores exceptions until await() 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 task
  • async = start a task that you plan to collect later

Mental Model

Think of launch and async like assigning work to people.

  • launch is like telling someone: "Please do this task." Later, you can ask: "Are you finished?" That is join().
  • async is like telling someone: "Please do this task and bring me the answer." Later, you ask: "What result did you get?" That is await().

So:

  • launch = work without a return value
  • join = wait for completion
  • async = work with a future result
  • await = 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 a T later"
Take Quiz

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:

  • launch returns a Job
  • the coroutine prints a message
  • join() waits for it to finish
  • no value is returned

In the second example:

  • async returns a Deferred<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

  1. runBlocking starts a coroutine scope and blocks the main thread until all work inside completes.
  2. async { ... } starts a new coroutine immediately.
  3. That coroutine pauses for 500 milliseconds because of delay(500).
  4. Meanwhile, the next line runs: println("Before await").
  5. The program reaches deferred.await().
  6. If the async coroutine has not finished yet, await() suspends until it does.
  7. After the delay, the async coroutine computes 10 + 20, producing 30.
  8. await() returns 30.
  9. 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)
  • launch is not designed to return values
  • async expresses 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
Take Quiz

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 completion
  • await() waits and returns the value

Use the one that matches the coroutine type and intent.

Comparisons

Featurelaunchasync
ReturnsJobDeferred<T>
Produces a valueNoYes
Waiting methodjoin()await()
Best forSide effects, actionsConcurrent computations, fetched results
Exception stylePropagates as coroutine failureStored until await()
Typical intent"Do this work""Compute this result"

join() vs await()

MethodUsed withWhat it does
join()JobWaits until coroutine finishes
await()Deferred<T>Waits until coroutine finishes and returns T

launch/join vs async/await

  • Choose launch/join for completion tracking
  • Choose async/await for 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.

Take Quiz

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

  • launch returns Job
  • async returns Deferred<T>
  • Deferred<T> is a kind of Job
  • join() waits for completion only
  • await() waits and gives back the result
  • Use launch for side effects
  • Use async for 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 to await()
  • Start multiple async tasks before awaiting if you want concurrency
  • Prefer structured scopes like coroutineScope or runBlocking

Quick memory trick

  • launch = launch an action
  • async = 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more