Why is catching exceptions inside a withTimeout block dangerous, and how should you handle TimeoutCancellationException correctly?
answer
- TimeoutCancellationException IS a CancellationException
- Broad catch(Exception) swallows the timeout — bug
- Rethrow CancellationException or catch narrowly
- withTimeoutOrNull catches only its own timeout
- Catch around withTimeout to react; NonCancellable for cleanup
basics
~10 sThe timeout works by throwing a special cancellation exception. If your try/catch swallows it, the timeout stops working and cleanup may break. Always let cancellation exceptions pass through, or use withTimeoutOrNull instead.
solid answer
~50 sWhen `withTimeout` fires, it cancels the inner coroutine, which surfaces as a `TimeoutCancellationException` — a subclass of `CancellationException`. If code inside the block does a broad `try { ... } catch (e: Exception) { ... }`, it captures that cancellation exception too, so the timeout never propagates and the coroutine keeps running as if nothing happened. The fix is to rethrow cancellation: `catch (e: CancellationException) { throw e }` before handling other exceptions, or catch only the specific exceptions you expect. To turn a timeout into a value instead of an exception, use `withTimeoutOrNull`, which catches only its own `TimeoutCancellationException`. Note that `TimeoutCancellationException` is only special inside the coroutine that `withTimeout` started — once it crosses the `withTimeout` boundary as a normal throw (from `withTimeout`, not `withTimeoutOrNull`), you can catch it like any exception to react to the deadline.
code
kotlin · 17 linesimport kotlinx.coroutines.*
suspend fun safe() {
try {
withTimeout(500) {
try {
doWork()
} catch (e: CancellationException) {
throw e // never swallow cancellation
} catch (e: Exception) {
recover(e)
}
}
} catch (e: TimeoutCancellationException) {
log("deadline exceeded")
}
}go deeper
Recognizes that a try/catch can break a timeout but may not explain why.
Explains the CancellationException subclassing and applies rethrow / narrow-catch patterns correctly.
Adds NonCancellable cleanup and distinguishes catching inside vs. outside the withTimeout boundary.
Codifies a team convention (lint/utility) so cancellation is never swallowed across the codebase.
## The mechanism `withTimeout` launches the block in a child scope and schedules a cancellation after the deadline. Cancellation is delivered by throwing `TimeoutCancellationException` at the next suspension point inside the block. Because `TimeoutCancellationException : CancellationException`, the coroutine framework recognizes it as *normal* cancellation rather than a failure. ## The trap: swallowing CancellationException A common bug: ```kotlin withTimeout(1000) { try { someSuspendingCall() } catch (e: Exception) { // BUG: also catches TimeoutCancellationException log.warn("ignored", e) } } ``` Here the `catch (e: Exception)` block intercepts the timeout's `TimeoutCancellationException`. The cancellation is suppressed, the block continues, and the timeout effectively does nothing. The same trap breaks any cooperative cancellation, not just timeouts. ## The correct patterns **Rethrow cancellation explicitly:** ```kotlin try { someSuspendingCall() } catch (e: CancellationException) { throw e // let cancellation/timeout propagate } catch (e: IOException) { handle(e) } ``` **Catch only what you expect** — narrow `catch` clauses (e.g. `IOException`) never touch `CancellationException`. **Use withTimeoutOrNull** when you just want a value on timeout: ```kotlin val r = withTimeoutOrNull(1000) { someSuspendingCall() } ?: default ``` ## Reacting to a timeout from outside If you want to run custom logic when `withTimeout` times out, catch it *around* the call (it throws a real exception there): ```kotlin try { withTimeout(1000) { work() } } catch (e: TimeoutCancellationException) { metrics.increment("timeout") throw DeadlineExceededException() } ``` This is safe because the exception escaping `withTimeout` is no longer cancelling the *current* scope — it's a value you observe. (Catching it inside the same coroutine that `withTimeout` cancels is what you must avoid.) ## Cleanup on timeout If the block holds resources, wrap cleanup so it still runs even though the coroutine is being cancelled. Use `try/finally`; for suspending cleanup that must run during cancellation, wrap it in `withContext(NonCancellable) { ... }` so it isn't immediately cancelled again.
- How do you run cleanup that suspends while the coroutine is being cancelled by a timeout?Wrap the suspending cleanup in withContext(NonCancellable) { ... } inside a finally block, so the cleanup isn't immediately cancelled again.
- Is it safe to catch TimeoutCancellationException around the withTimeout call itself?Yes. At that point it's a thrown exception you observe, not active cancellation of the current scope, so reacting (logging, mapping to a domain error) is fine.
saying these in an interview costs you the question
- Using catch(Exception) inside the block without rethrowing CancellationException
- Not knowing TimeoutCancellationException extends CancellationException
- Doing suspending cleanup without NonCancellable
- Believing the timeout still fires even if its exception is swallowed
- Treating all CancellationExceptions as errors to report