skip to content

withContext

withContext suspends, runs a block on a different dispatcher, and returns its result, which makes it the idiomatic way to move blocking I/O onto Dispatchers.IO. Interviewers contrast it with async: withContext is sequential, not concurrent.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does withContext do, and how would you use it to run blocking I/O off the main thread?

level: juniorimportance: must knowfreq 80%

answer

  1. Suspends, runs block in new context, returns result
  2. withContext(Dispatchers.IO) = offload blocking I/O
  3. No new coroutine, runs sequentially
  4. Resumes on caller's dispatcher automatically
  5. Makes suspend functions main-safe

basics

~20 s

withContext runs a block of suspending code on a different thread pool and waits for its result. You wrap blocking I/O in withContext(Dispatchers.IO) so it does not freeze the main thread, then it returns the value.

solid answer

~40 s

withContext(context) is a suspending function that switches the coroutine's CoroutineContext (typically the dispatcher) for the duration of a block, runs the block, and returns the block's last expression as a value. The classic idiom is withContext(Dispatchers.IO) { ... } to move blocking calls (JDBC, file reads, blocking HTTP) off the main/UI thread or a constrained dispatcher onto the I/O thread pool. Unlike launch/async it does not create a new concurrent coroutine — it runs sequentially in the same coroutine and suspends the caller until done. When the block finishes, the coroutine resumes back on the original dispatcher automatically. It is the standard way a suspend function makes itself main-safe: callers can invoke it from any dispatcher and it offloads internally.

code

kotlin · 3 lines
kotlin
suspend fun fetchConfig(): String = withContext(Dispatchers.IO) {
    java.io.File("config.txt").readText() // blocking, but on the IO pool
}

go deeper

for a junior

Knows withContext(Dispatchers.IO) offloads blocking I/O and returns a result.

for a middle

Explains main-safety and that it resumes on the original dispatcher automatically.

for a senior

Contrasts with launch/async, notes it is a cancellable suspension point and the cheapest single-offload primitive.

for a principal

Frames main-safety as a library design contract and reasons about dispatcher selection for throughput.

## What `withContext` is `withContext(context: CoroutineContext, block: suspend () -> T): T` is a **suspending function** from `kotlinx.coroutines`. It runs `block` with the given context elements merged onto the current coroutine's context, **suspends the calling coroutine until the block completes**, and **returns the block's result**. The most common element you pass is a **`CoroutineDispatcher`** — the object that decides which thread the coroutine runs on: - `Dispatchers.Default` — CPU-bound work, pool sized to CPU cores. - `Dispatchers.IO` — blocking I/O (JDBC, file, blocking sockets); large elastic pool. - `Dispatchers.Main` — the UI thread (Android/JavaFX/Swing). - `Dispatchers.Unconfined` — rarely used; starts in the caller thread. ## Why it matters: main-safety A `suspend` function should be **main-safe** — callable from any dispatcher without blocking it. You achieve that by wrapping blocking code internally: ```kotlin suspend fun loadUser(id: Long): User = withContext(Dispatchers.IO) { jdbcTemplate.queryForObject(/* blocking */) // runs on IO pool } ``` The caller can run on `Dispatchers.Main`; `withContext` hops to `IO`, runs the blocking call, then **resumes back on the caller's original dispatcher** when the block ends. You do not switch back manually. ## How it differs from `launch`/`async` - `launch` returns a `Job`, `async` returns a `Deferred<T>` — both start a **new concurrent coroutine**. - `withContext` starts **no new coroutine**; it runs the block **sequentially** in the same coroutine and returns the value directly. There is nothing to `await`. So `val x = async(ctx) { f() }.await()` is functionally close to `val x = withContext(ctx) { f() }`, but `withContext` is the idiomatic, cheaper choice for a single sequential offload. ## Return value and cancellation - The block's **last expression** is the return value. - `withContext` is a **cancellable suspension point**: if the parent coroutine is cancelled it throws `CancellationException`. ## Recap - Use it to **change dispatcher** for a block and get a result. - The idiom for offloading blocking work is `withContext(Dispatchers.IO) { ... }`.

  • After withContext(Dispatchers.IO) { ... } returns, which dispatcher does the coroutine continue on?
    It resumes on whatever dispatcher the caller was on before the call; the IO switch only applies inside the block.
  • Does withContext start a new coroutine?
    No. It runs the block in the same coroutine, just with a modified context, and suspends until it completes.

Like stepping into a different room to do a noisy task, then walking back to your desk with the result.

saying these in an interview costs you the question

  • Thinking withContext starts a concurrent coroutine like launch
  • Saying you must manually switch the dispatcher back afterward
  • Using Dispatchers.IO for CPU-bound work instead of Default
  • Believing withContext blocks the calling thread (it suspends, not blocks)

context

open as a page

When would you choose withContext over async { }.await() for running work on another dispatcher?

level: middleimportance: must knowfreq 65%

basics

~10 s

Use withContext when you just need to run one block somewhere else and get its result in sequence. Use async/await only when you want two or more things running at the same time.

open as a page

Why is Dispatchers.IO the standard target for withContext when calling blocking APIs, and when would Dispatchers.Default be wrong or right?

level: middleimportance: should knowfreq 55%

basics

~20 s

Dispatchers.IO has a large pool meant for tasks that wait on the network or disk, so many can block at once. Dispatchers.Default has only as many threads as CPU cores, good for heavy calculations but easily clogged by blocking calls.

open as a page

How does withContext interact with cancellation and the parent CoroutineContext, including the Job and CoroutineExceptionHandler?

level: seniorimportance: should knowfreq 40%

basics

~20 s

withContext keeps you inside the same parent coroutine. You can override things like the dispatcher, but it still uses the parent's Job, so if the parent is cancelled the block is cancelled too, and errors propagate up normally.

open as a page

A coroutine is cancelled; you need a final cleanup suspend call to still run. How do you do that with withContext, and what are the pitfalls?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

Wrap the cleanup in withContext(NonCancellable) inside a finally block. That lets the suspending cleanup run even though the coroutine is already cancelled. Keep it short, because nothing can stop it.

open as a page