TLDR: The withTimeout function doesn’t cancel the execution of the block you pass it. It throws a TimeoutCancellationException, which, when left uncaught, cancels the invoking coroutine.

The withTimeoutOrNull behaves as expected, canceling only the block, and returning null in case the timeout was exceeded.

The kotlinx.coroutines team is aware of this issue.


In Kotlin coroutines, the withTimeout function can be used to constrain the execution of your code to a specific timeout. Its signature looks as follows:

withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T

…you specify a timeout, and a suspending code block that is an extension on CoroutineScope. As you might correctly guess, the timeout is realized via the regular cancellation mechanism in Kotlin: That is, after the timeout has expired, the block throws as a TimeoutCancellationException, a subclass of CancellationException.

However, you might see this TimeoutCancellationException appear in a place where you wouldn’t expect it. Consider the following code snippet and its output (in its entirety):

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        this.coroutineContext.job.invokeOnCompletion {
            println("Job is completing: $it")
        }
        launch {
            while (true) {
                println("Alive!")
                delay(500)
            }
        }
        withTimeout(500) {
            println("Doing some too-long-running-task that will timeout")
            delay(2000)
            // won't reach here
        }
    }
}

// Doing some too-long-running-task that will time out
// Alive!
// Job is completing: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 500 ms
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 500 ms

This demonstrates the part that might clash with your intuition: It’s not the timeout block that gets canceled; it’s the coroutine scope in which withTimeout was called that is canceled. In the case of this code snippet, that means the application as a whole terminates. In the case of a larger application, you might just see your coroutines silently canceled.

Fix #1: Catch the TimeoutCancellationException

You can remedy this by explicitly catching the TimeoutCancellationException:

try {
    withTimeout(500) {
        println("Doing some too-long-running-task that will time out")
        delay(2000)
        // won't reach here
    }
} catch (t: TimeoutCancellationException) {
    println("Timed out")
    // TODO: Special handling for nested `withTimeout` calls?
}

When catching the exception, other coroutines of the scope from which you called withTimeout will keep running, since the CancellationException (or, more precisely, the TimeoutCancellationException) was prevented from reaching the scope. Forgetting to wrap the withTimeout function in this try-catch block will cause the surrounding scope to be canceled, which is not what you may have expected!

After repeating the mantra “don’t catch CancellationException without rethrowing it” to myself for ages, it does feel a little bit odd to now catch a subtype of CancellationException and not propagate it further. Hence, I prefer fix #2.

Fix #2: Use the withTimeoutOrNull sibling function

Alternatively, you can use the sibling function withTimeoutOrNull. In the typical Kotlin pattern, where withTimeout throws an exception, withTimeoutOrNull returns null if the timeout is exceeded. As such, it doesn’t suffer from the same trickiness that stems from TimeoutCancellationException being a subtype of CancellationException. If you look into the implementation of withTimeoutOrNull, you’ll recognize that it does practically the same thing we’ve done in the snippet above: It takes care of catching the TimeoutCancellationException to prevent it from propagating further, and returns null. In addition the snippet above, its implementation also ensures that the TimeoutCancellationException really came from this specific coroutine, and not from a nested withTimeout call:

public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
    if (timeMillis <= 0L) return null

    var coroutine: TimeoutCoroutine<T?, T?>? = null
    try {
        return suspendCoroutineUninterceptedOrReturn { uCont ->
            val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
            coroutine = timeoutCoroutine
            setupTimeout<T?, T?>(timeoutCoroutine, block)
        }
    } catch (e: TimeoutCancellationException) {
        // Return null if it's our exception, otherwise propagate it upstream (e.g., in case of nested withTimeouts)
        if (e.coroutine === coroutine) {
            return null
        }
        throw e
    }
}

Personally, I’d probably just opt for the withTimeoutOrNull function: It future-proofs me from any changes that might be made to the withTimeout function regarding whether the thrown exception stays a subtype of CancellationException or not, and it saves me having to manage a separate try-catch.

If you only look at the signature, it’s tempting to interpret withTimeout as introducing its own coroutine scope that will be canceled when the timeout expires, but that’s not the actual behavior – the chances are good that you’ll have an easier time just using withTimeoutOrNull. Something to look out for the next time you’re building timeouts into your concurrent code!