Question
In Kotlin coroutines, withContext(context) runs a block using another coroutine context and then resumes in the original context. async(context) starts a child coroutine and returns a Deferred; calling await() suspends until that coroutine produces its result.
Consider these two versions:
launch {
block1()
val returned = async(context) {
block2()
}.await()
block3()
}
launch {
block1()
val returned = withContext(context) {
block2()
}
block3()
}
Assuming the surrounding launch uses one dispatcher and context supplies another dispatcher, block1() and block3() run in the surrounding coroutine's context, while block2() runs in context. In both examples, execution reaches the blocks in the order block1(), block2(), then block3().
Is withContext generally preferable to immediately calling await() on async, since it avoids creating a separate child coroutine? When is async and await the better choice?
Short Answer
You will learn the difference between switching context for one awaited operation and starting a separate concurrent operation. In most cases, async { ... }.await() should be replaced with withContext { ... } when the result is needed immediately. Use async when you can start multiple independent tasks and await their results later.
Concept
withContext and async can both run code using a different dispatcher, such as Dispatchers.IO for blocking I/O or Dispatchers.Default for CPU-intensive computation. However, they represent different intentions.
withContext(context) { ... }suspends the current coroutine while the block runs in the supplied context. It returns the block's result directly.async(context) { ... }creates a new child coroutine and immediately returns aDeferred<T>, which is a future result.await()suspends until that particularDeferredhas completed and then returns its value.
When await() immediately follows async, there is no useful opportunity for the caller to do other work or start another independent task. The code is therefore sequential from the caller's perspective, and withContext expresses that intent more directly.
// Prefer this for one operation whose result is needed now.
val user = withContext(Dispatchers.IO) {
userRepository.loadUser(id)
}
async matters when work can overlap. Start several independent operations first, then await their results. This can reduce total elapsed time when the operations use different threads or spend time waiting for I/O.
Also, avoid assuming that launch always uses Dispatchers.Default. A coroutine inherits the context of its scope unless a dispatcher is explicitly supplied. In modern Kotlin, use explicit dispatcher names such as Dispatchers.Default and Dispatchers.IO rather than relying on older names such as CommonPool.
Mental Model
Think of withContext as taking your current task to a different work desk temporarily:
- You carry the task to the I/O desk.
- You complete that piece of work there.
- You return to your original desk with the result.
You never handed the task to somebody else; you simply worked at a different desk.
Think of async as assigning a task to another worker and receiving a claim ticket (Deferred) in return. You can continue assigning or doing other work. Later, you show the ticket with await() to collect the result.
If you assign one task and stand at the counter waiting for it immediately, using another worker provides little benefit. That is the async { ... }.await() case.
Syntax and Examples
withContext returns the result of its block directly:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun readSettings(): String = withContext(Dispatchers.IO) {
// Blocking file or database work belongs on Dispatchers.IO.
"dark-mode=true"
}
Because it is a suspend function, withContext can be called only from a coroutine or another suspend function.
async returns a Deferred<T>:
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
suspend fun loadGreeting(): String = coroutineScope {
val greetingDeferred = async {
"Hello"
}
greetingDeferred.await()
}
The useful async pattern starts independent work before awaiting it:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
suspend fun loadDashboard(userId: String): Dashboard = coroutineScope {
val profileDeferred = async(Dispatchers.IO) {
loadProfile(userId)
}
val notificationsDeferred = async(Dispatchers.IO) {
loadNotifications(userId)
}
Dashboard(
profile = profileDeferred.await(),
notifications = notificationsDeferred.await()
)
}
Both requests are started before either result is awaited. If they are independent, their waiting time can overlap.
Step by Step Execution
Consider this sequential context switch:
suspend fun createLabel(): String {
val number = withContext(Dispatchers.Default) {
20 + 22
}
return "Answer: $number"
}
Execution proceeds as follows:
createLabel()starts in its caller's coroutine context.withContext(Dispatchers.Default)suspends the current coroutine while its block is scheduled onDispatchers.Defaultwhen a dispatcher switch is needed.- The block calculates
20 + 22and produces42. - The coroutine resumes in its original context, with
numberset to42. createLabel()returns"Answer: 42".
Now compare independent concurrent work:
suspend fun combinedValue(): Int = coroutineScope {
val first = async(Dispatchers.Default) {
calculateFirst()
}
val second = async(Dispatchers.Default) {
calculateSecond()
}
first.await() + second.await()
}
- The first child coroutine is created and can begin
calculateFirst(). - The second child coroutine is created and can begin
calculateSecond(). first.await()waits only if the first calculation is unfinished.second.await()waits only if the second calculation is unfinished.- The values are added after both tasks finish.
The order in which the calculations finish is not guaranteed. Only the final addition requires both results.
Real World Use Cases
Use withContext when one operation must run on a particular dispatcher and the next line needs its result:
- Reading a file, querying a traditional blocking database driver, or making another blocking call on
Dispatchers.IO. - Performing a CPU-heavy transformation, such as image processing or parsing a large document, on
Dispatchers.Default. - Switching to
Dispatchers.Mainto update an Android UI after background work. - Calling a repository method whose result is immediately needed for validation or a response.
Use async when multiple tasks are independent:
- Load a user profile, permissions, and notifications for one screen at the same time.
- Fetch several independent API resources before producing one server response.
- Run separate calculations over independent data partitions.
- Begin a background operation now and collect its result only after other work has been done.
Do not use async merely to make a single operation "asynchronous." A suspending function can already avoid blocking the calling thread.
Real Codebase Usage
In production Kotlin code, withContext is often placed at the boundary around blocking or dispatcher-specific work:
class ReportRepository {
suspend fun loadReport(id: String): Report = withContext(Dispatchers.IO) {
database.readReport(id)
}
}
This keeps callers simple: they receive a normal Report result rather than a Deferred<Report>.
For concurrent composition, create async tasks inside a structured scope such as coroutineScope. This ensures the parent waits for its children and cancellation is handled predictably.
suspend fun loadPageData(id: String): PageData = coroutineScope {
val account = async(Dispatchers.IO) { accountApi.getAccount(id) }
val orders = async(Dispatchers.IO) { ordersApi.getOrders(id) }
PageData(account.await(), orders.await())
}
A common validation pattern uses a guard clause before switching context:
suspend fun findDocument(id: String): Document? {
if (id.isBlank()) return null
return withContext(Dispatchers.IO) {
documentStore.find(id)
}
}
In well-designed applications, dispatcher selection may be injected rather than hard-coded. This makes tests easier because a test dispatcher can replace Dispatchers.IO or Dispatchers.Default.
Common Mistakes
Using async followed by immediate await
// Usually unnecessary extra coroutine creation.
val data = async(Dispatchers.IO) {
loadData()
}.await()
Prefer:
val data = withContext(Dispatchers.IO) {
loadData()
}
Awaiting before starting other independent work
val profile = async { loadProfile() }.await()
val orders = async { loadOrders() }.await()
This is effectively sequential. Start both first:
val profileTask = async { loadProfile() }
val ordersTask = async { loadOrders() }
val profile = profileTask.await()
val orders = ordersTask.await()
Treating async as fire-and-forget work
If no code awaits a Deferred, the result is ignored. More importantly, exceptions can be delayed until the deferred is awaited, depending on the surrounding coroutine structure. For intentional fire-and-forget work, launch may better communicate the intent, but it still needs a lifecycle-aware scope and error-handling strategy.
Calling blocking code on the wrong dispatcher
// Bad if readFileBlocking() blocks a thread on the main/UI dispatcher.
val text = readFileBlocking()
Wrap blocking work in withContext(Dispatchers.IO). Do not switch dispatchers unnecessarily when an API is already non-blocking and suspending.
Creating unstructured coroutines
Avoid creating work in a detached scope just to get a Deferred:
// Avoid in ordinary application logic.
val task = CoroutineScope(Dispatchers.IO).async { loadData() }
Prefer coroutineScope, a lifecycle-owned scope, or an existing structured scope so cancellation and failures have a clear owner.
Comparisons
| Feature | withContext | async + await |
|---|---|---|
| Primary purpose | Run one block in another context | Start a child coroutine and represent its future result |
| Return value | Returns T directly | Returns Deferred<T>; await() returns T |
| Creates a child coroutine | No; it changes context for the current coroutine's work | Yes |
| Best when | The result is needed immediately | Independent work can run before its result is needed |
| Enables overlapping work | Not by itself | Yes, when multiple tasks are started before awaiting |
| Typical dispatcher use | withContext(Dispatchers.IO) { blockingCall() } | async(Dispatchers.IO) { independentCall() } |
launch differs from both because it returns a Job, not a value. Use it for work where completion or cancellation matters but no computed result must be returned.
| API | Returns | Typical intent |
|---|---|---|
withContext | T | Switch context and get one result |
async | Deferred<T> | Compute a result concurrently |
launch | Job | Start coroutine work without a result |
Cheat Sheet
- Use
withContext(dispatcher) { ... }when you need the block's result immediately. withContextis a suspending function and returns the block's result directly.- Use
async { ... }to obtainDeferred<T>when the result can be awaited later. - Start all independent
asynctasks before callingawait()to gain concurrency. async { work() }.await()is usually better written aswithContext { work() }when the only purpose is a context switch.await()suspends a coroutine; it does not block the underlying thread.- Use
Dispatchers.IOfor blocking I/O andDispatchers.Defaultfor CPU-bound work. - A coroutine inherits its scope's context unless you provide another context.
- Prefer structured scopes such as
coroutineScope { ... }for concurrentasyncoperations. - Do not assume
asyncmakes work faster. Dependent work remains sequential, and excessive concurrency can hurt performance.
FAQ
Is async { }.await() the same as withContext { }?
They can produce the same final value and apparent sequence when await() is immediate, but they are not the same mechanism. async creates a child coroutine and a Deferred; withContext runs the block as part of the current coroutine's structured execution.
Should I always replace immediate async().await() with withContext()?
Usually, yes, if you only need to switch dispatcher/context and need the result immediately. It is clearer and avoids the unnecessary Deferred and child coroutine.
Does await() block a thread in Kotlin?
No. await() suspends the coroutine. The underlying thread can run other coroutine work while the awaited task is incomplete.
When does async actually run in parallel?
It can run concurrently with other coroutines when its dispatcher has available threads and the work is independent. Parallel execution is possible for CPU work on a multi-threaded dispatcher; I/O tasks often overlap their waiting time.
Can I call withContext outside a coroutine?
No. It is a suspending function, so it must run from a coroutine or another suspend function.
What happens if an async child fails?
In normal structured concurrency, a failing child can cancel its parent scope and sibling coroutines. Handle failures deliberately where appropriate, and avoid detached scopes that make errors difficult to track.
Do I need Dispatchers.IO for every network call?
Not necessarily. Many modern network clients expose non-blocking suspending APIs. Use Dispatchers.IO for genuinely blocking I/O APIs, such as a blocking file or database call.
Mini Project
Description
Build a small dashboard loader that retrieves two independent pieces of data concurrently. The project demonstrates why async is useful when work can overlap, while withContext remains useful for a single dispatcher-specific operation.
Goal
Return a dashboard summary after loading a profile and notifications concurrently, then format the result on the caller's context.
Requirements
Create a DashboardSummary data class with a user name and notification count.
Create two suspending functions that simulate independent I/O operations.
Use a structured coroutine scope to start both operations before awaiting either result.
Use withContext(Dispatchers.Default) to format the final summary.
Return the completed DashboardSummary.
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.