skip to content

Why is catching exceptions inside a withTimeout block dangerous, and how should you handle TimeoutCancellationException correctly?

level: middleimportance: must knowfreq 55%

answer

  1. TimeoutCancellationException IS a CancellationException
  2. Broad catch(Exception) swallows the timeout — bug
  3. Rethrow CancellationException or catch narrowly
  4. withTimeoutOrNull catches only its own timeout
  5. Catch around withTimeout to react; NonCancellable for cleanup

basics

~10 s

The 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 s

When `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 lines
kotlin
import 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

for a junior

Recognizes that a try/catch can break a timeout but may not explain why.

for a middle

Explains the CancellationException subclassing and applies rethrow / narrow-catch patterns correctly.

for a senior

Adds NonCancellable cleanup and distinguishes catching inside vs. outside the withTimeout boundary.

for a principal

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

context