Should cleanup inside withTimeout run in NonCancellable, or is a finally block enough?
0 reputation · 17 May 2025, 06:54 UTC
0 reputation · 17 May 2025, 06:54 UTC
I'm designing a Kotlin coroutine that performs a network fetch wrapped in withTimeout, and on timeout I need to release a resource (close a connection and write a short audit log entry). I understand that withTimeout throws TimeoutCancellationException, which is a subtype of CancellationException, and that once the coroutine is cancelled, further suspending calls in a normal finally block will immediately re-throw.
The documented behavior I've seen is that cleanup requiring suspension should be wrapped in withContext(NonCancellable), while purely synchronous cleanup can stay in a plain finally. What I'm unsure about is the boundary: my audit logging goes through a suspending API, and I also catch exceptions around the block to distinguish timeout from other failures.
Specifically:
TimeoutCancellationException outside the withTimeout call, is the surrounding coroutine still considered cancelled at that point, or only the timed block?withContext(NonCancellable) the correct and sufficient pattern here, or are there pitfalls when the parent scope itself is also being cancelled?CancellationException after cleanup to preserve structured concurrency, even when I handled the timeout case deliberately?Assume kotlinx-coroutines 1.7+ on the JVM. I'd like to confirm the intended pattern before committing to it.
31775 reputation · 17 May 2025, 13:56 UTC
For suspending cleanup (e.g., an audit‑log write that suspends) you must wrap it in withContext(NonCancellable) inside a finally block. A plain finally alone is not enough because any suspending call will immediately re‑throw CancellationException once the withTimeout block is cancelled.
finally { withContext(NonCancellable) { /* suspending close / audit log */ } }.TimeoutCancellationException outside the withTimeout call, perform the cleanup as above, then re‑throw the caught exception (unless you intentionally want to suppress it).finally block – it does not need NonCancellable.When withTimeout expires it throws TimeoutCancellationException, which cancels the coroutine executing the timeout block. Any suspending point in a normal finally will see that cancellation and immediately propagate CancellationException, preventing the cleanup from finishing. withContext(NonCancellable) creates a child coroutine that ignores cancellation, allowing suspending cleanup to run to completion.
Catching TimeoutCancellationException outside withTimeout leaves the outer coroutine uncancelled; only the timed block was cancelled. After cleanup you should usually re‑throw the exception to preserve structured concurrency, unless your design explicitly wants to swallow the timeout.
The only situation that could change the recommendation is if you also need the cleanup to run even when the parent coroutine scope is already cancelled. In that case you would need to know whether the parent scope’s cancellation should be ignored for the cleanup. If you can confirm that the parent scope is not cancelled (or that you deliberately want the cleanup to run despite it), the pattern above is sufficient. Otherwise, you would need to make the parent scope non‑cancellable as well.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.