CancellationException Pitfalls

Swallowing Cancellation

A catch (e: Exception) also catches CancellationException. The coroutine then "forgets" that it was cancelled and keeps going. The same happens with runCatching { }.

Example

The bad loop keeps running after cancel; each delay fails immediately and is swallowed.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val bad = launch {
        repeat(4) { i ->
            try {
                delay(100)
                println("bad: tick $i")
            } catch (e: Exception) {
                println("bad: swallowed ${e::class.simpleName}")
            }
        }
        println("bad: loop finished anyway")
    }
    delay(150)
    bad.cancelAndJoin()

    val good = launch {
        repeat(4) { i ->
            try {
                delay(100)
                println("good: tick $i")
            } catch (e: CancellationException) {
                throw e                       // always rethrow
            } catch (e: Exception) {
                println("good: real error ${e.message}")
            }
        }
        println("good: never reached")
    }
    delay(150)
    good.cancelAndJoin()
    println("Done")
}

Output

bad: tick 0
bad: swallowed JobCancellationException
bad: swallowed JobCancellationException
bad: swallowed JobCancellationException
bad: loop finished anyway
good: tick 0
Done

Rules

  • Catch specific exceptions (IOException, HttpException) instead of Exception.
  • If you must catch Exception, rethrow CancellationException first, or call coroutineContext.ensureActive() in the catch.
  • Do not throw CancellationException yourself to signal errors; it is treated as a normal cancel and will not reach exception handlers.