When wrapping resource acquisition in withTimeout, how can a timeout leak resources, and how do you prevent it?
answer
- Timeout can cancel right after acquire, before capture => leak
- Acquire outside withTimeout; bound only the work
- try/finally runs on the cancellation path
- Suspending cleanup needs withContext(NonCancellable)
- use { } for Closeable resources
basics
~20 sIf a timeout cancels the code right after a resource is opened but before you store or release it, the resource can leak. Acquire and release inside try/finally, and make sure cleanup still runs during cancellation.
solid answer
~50 sA timeout can cancel the block at any suspension point, including the instant *after* a resource is opened but before it's assigned to a variable you'd later close. The classic leak: `val res = withTimeout(t) { openResource() }` — if cancellation lands as `openResource()` returns, the value is dropped and never closed. The docs explicitly warn about this. Mitigations: (1) acquire the resource *outside* `withTimeout` and only do the time-bounded work inside; (2) use `try/finally` so close runs on the cancellation path; (3) if close itself suspends, wrap it in `withContext(NonCancellable) { ... }` so the already-cancelling coroutine can still complete cleanup. Combine with `use { }` for `Closeable` resources where possible. The principle: never let the only reference to a resource live solely inside a cancellable region whose result might be discarded by a timeout.
code
kotlin · 10 linesimport kotlinx.coroutines.*
suspend fun query(): Result = run {
val conn = openConnection() // acquire OUTSIDE the deadline
try {
withTimeout(2000) { conn.runQuery() } // only the work is bounded
} finally {
withContext(NonCancellable) { conn.close() } // suspending close still completes
}
}go deeper
Likely unaware of the acquire/cancel race and would write the leaky pattern.
Uses try/finally and knows finally runs on cancellation for non-suspending close.
Owns resource lifetime outside the deadline and routes suspending cleanup through NonCancellable.
Establishes resource-ownership conventions and reviews APIs so deadlines can never leak connections/handles.
## The leak scenario `withTimeout` cancels its block at the next suspension point after the deadline. Cancellation can be delivered *after* a side effect (opening a file, acquiring a connection) but *before* the value is returned and captured by the caller. The kotlinx.coroutines docs call this out directly with an example like: ```kotlin var acquired = 0 class Resource { init { acquired++ }; fun close() { acquired-- } } withTimeout(60) { val r = Resource() // acquired++ delay(70) // cancelled here -> r is never closed -> leak r.close() } ``` If the timeout fires during `delay`, `r.close()` never runs and `acquired` stays incremented. ## Fix 1 — acquire outside, bound only the work ```kotlin val r = Resource() try { withTimeout(60) { useResource(r) } // only the usage is time-bounded } finally { r.close() // always runs } ``` The resource's lifetime is owned by a non-cancellable `try/finally`, so a timeout can't strand it. ## Fix 2 — try/finally inside, close on cancel path ```kotlin withTimeout(60) { val r = Resource() try { delay(70) } finally { r.close() // runs even when cancelled (close is non-suspending) } } ``` `finally` blocks run during cancellation, so a *non-suspending* close is safe here. ## Fix 3 — NonCancellable for suspending cleanup If cleanup itself suspends (e.g. an async flush/close), the coroutine is already cancelling, so the next suspension would immediately throw. Wrap suspending cleanup so it's allowed to finish: ```kotlin withTimeout(60) { val conn = openConnection() try { conn.query() } finally { withContext(NonCancellable) { conn.closeAsync() } // suspending close still runs } } ``` `NonCancellable` is a special `Job` that ignores cancellation, so the cleanup completes. ## Closeable helper For `java.io.Closeable`/`AutoCloseable`, `use { }` guarantees `close()` in a `finally`; combine with the acquire-outside pattern for the strongest guarantee. There's also `Closeable.use` / Kotlin's `use` extension. ## Principle Never let the *sole* reference to an acquired resource exist only inside a cancellable region whose result a timeout could discard. Own the lifetime in a non-cancellable scope, and route suspending cleanup through `NonCancellable`.
- Why can't you rely on a finally that suspends to clean up during a timeout?The coroutine is already cancelling, so the suspending call in finally immediately throws CancellationException and cleanup is skipped. Wrap it in withContext(NonCancellable).
- Is acquiring the resource inside withTimeout ever acceptable?Yes, if you guard it with try/finally and the close is non-suspending (or NonCancellable), so cancellation can't strand it before close runs.
saying these in an interview costs you the question
- Assigning an acquired resource only inside withTimeout with no try/finally
- Doing a suspending close in finally without NonCancellable
- Believing cancellation never happens between acquire and capture
- Not knowing NonCancellable exists
- Ignoring use { } for Closeable resources