How do withTimeout and withTimeoutOrNull interact with structured concurrency and nested deadlines?
answer
- Timeout block = child CoroutineScope (structured concurrency)
- On timeout, all children cancelled and awaited
- Nested timeouts: tightest live deadline wins
- withTimeoutOrNull catches only its OWN timeout, not outer
- Dispatcher inherited; switch with withContext
basics
~10 sA timeout cancels everything started inside its block, including child coroutines. If you nest timeouts, the shorter effective deadline wins. The timeout block behaves like a normal coroutine scope.
solid answer
~50 s`withTimeout`/`withTimeoutOrNull` create a child `CoroutineScope` governed by structured concurrency. Any coroutines you `launch`/`async` inside the block are children of that scope, so when the timeout fires it cancels them all and waits for them to finish cancelling before completing. When timeouts nest, the **inner deadline is bounded by the outer**: if the outer expires first, the inner block is cancelled even though its own timer hasn't fired. A `withTimeoutOrNull` returns `null` for its own timeout, but if the *outer* timeout cancels it, the `TimeoutCancellationException` belongs to the outer scope and propagates up — the inner `withTimeoutOrNull` does not convert it to `null` because it only catches its own. The block also runs on the inherited dispatcher unless you switch with `withContext`. In short: timeouts are scope-aware, compose by structured concurrency rules, and the tightest live deadline dominates.
code
kotlin · 11 linesimport kotlinx.coroutines.*
suspend fun example() {
try {
withTimeout(100) { // outer deadline dominates
withTimeoutOrNull(5000) { delay(10_000) } // does NOT shield from outer
}
} catch (e: TimeoutCancellationException) {
println("outer deadline hit") // this runs
}
}go deeper
Knows the block cancels its work but not the nesting/scope subtleties.
Explains structured-concurrency child cancellation and the tightest-deadline-wins rule.
Reasons about withTimeoutOrNull vs outer timeouts, dispatcher inheritance, and supervisorScope interplay.
Designs deadline-propagation strategy across service boundaries so nested timeouts compose predictably.
## A timeout is a coroutine scope `withTimeout(t) { ... }` runs the block in a new child scope with a `Job` that's cancelled at the deadline. Because of **structured concurrency**, every coroutine you start inside (`launch`, `async`, `coroutineScope`) is a child of that scope: ```kotlin withTimeout(1000) { val a = async { fetchA() } val b = async { fetchB() } a.await() + b.await() // if the deadline hits, both a and b are cancelled } ``` When the timeout fires, the scope cancels all children and **waits** for their cancellation to complete before the timeout itself throws — so you don't leak runaway children. ## Nested timeouts: tightest deadline wins ```kotlin withTimeout(100) { // outer withTimeout(5000) { // inner asks for 5s delay(10_000) } } ``` The outer deadline (100 ms) is reached first. It cancels the inner block, even though the inner 5 s timer never expires. The effective deadline at any depth is the **minimum** of all enclosing live deadlines. ## withTimeoutOrNull only catches its own timeout ```kotlin val r = withTimeout(100) { withTimeoutOrNull(5000) { delay(10_000) } // outer cancels this } ``` Here the cancellation comes from the **outer** `withTimeout`. The inner `withTimeoutOrNull` does not swallow it (it only converts *its own* `TimeoutCancellationException` to `null`), so the outer's `TimeoutCancellationException` propagates and `withTimeout(100)` throws. Don't assume an inner `withTimeoutOrNull` shields you from an outer deadline. ## Dispatcher inheritance The block runs on the **inherited** `CoroutineContext`/dispatcher. To bound work on a different dispatcher, nest `withContext`: ```kotlin withTimeout(2000) { withContext(Dispatchers.IO) { blockingFriendlyWork() } } ``` ## Combining with coroutineScope/supervisorScope If a child started with `launch` fails inside the timeout block, normal failure propagation applies (a failing child cancels siblings) unless you use `supervisorScope`. The timeout adds a *deadline* dimension on top of these existing structured-concurrency rules — it does not change how failures propagate. ## Summary - The timeout block is a real child scope; children are cancelled and awaited on timeout. - Nested deadlines compose; the minimum live deadline dominates. - `withTimeoutOrNull` only neutralizes its *own* timeout, not an outer one. - Dispatcher is inherited; switch with `withContext`.
- If you launch two async children inside withTimeout and the deadline fires, what happens to them?Both are cancelled as children of the timeout scope, and the timeout waits for their cancellation to complete before throwing.
- Does an inner withTimeoutOrNull protect you from an outer withTimeout's deadline?No. withTimeoutOrNull only converts its own TimeoutCancellationException to null; an outer timeout's exception belongs to the outer scope and propagates up.
saying these in an interview costs you the question
- Thinking children survive the timeout
- Assuming a longer inner timeout can override a shorter outer one
- Believing inner withTimeoutOrNull swallows an outer timeout
- Not knowing the dispatcher is inherited
- Confusing failure propagation rules with deadline rules