Don't swallow CancellationException

Coroutines Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Spot the bug

The user leaves the screen during the load, yet the cache is still overwritten with an empty list (saveToDisk writes the file without suspending). Which line causes it?

  1. viewModelScope.launch {
  2. val items = try {
  3. api.feed()
  4. } catch (e: Exception) {
  5. emptyList()
  6. }
  7. saveToDisk(items)
  8. }
Hint 1 of 2

What exception does cancellation throw?

Hint 2 of 2

Is it a subtype of Exception?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 4 } catch (e: Exception) {

catch (e: Exception) also catches the CancellationException, so the coroutine falls through, gets an empty list and runs the save. Catch IOException or rethrow cancellation.

30-second answer

Cancellation is delivered as a CancellationException. A broad catch (e: Exception) catches it too, so a cancelled coroutine carries on as if nothing happened, keeps working after the screen is gone, and can write stale data. Catch the specific exceptions you expect, or rethrow CancellationException.

Practise saying it · 35 s. No recording, just a timer

Explain why catching Exception in a coroutine is risky.

Say your answer out loud, then check which of these points you covered:

  • Cancellation is a CancellationException
  • catch Exception swallows it
  • runCatching has the same issue
  • Catch specific types or rethrow

Deep dive

runCatching has the same problem: it catches everything, including cancellation.

If you must catch broadly, rethrow: catch (e: Exception) { if (e is CancellationException) throw e ... }, or call ensureActive() in the catch.

A TimeoutCancellationException from withTimeout is a CancellationException too.

Swallowing it doesn't stop the job being cancelled; the next suspension point throws again, but code in between runs on borrowed time.

A cancellation-safe wrapper Compile-checked · Kotlin 2.4.21
suspend inline fun <T> safeCall(block: () -> T): Result<T> =
    try {
        Result.success(block())
    } catch (e: CancellationException) {
        throw e
    } catch (e: Exception) {
        Result.failure(e)
    }

Common mistakes

  • Wrapping suspend calls in runCatching and ignoring cancellation.
  • Logging cancellation as an error in crash reports.
  • Thinking catching cancellation is harmless because the job is cancelled anyway.

Senior take & follow-ups

Interviewers use this to see if you know cancellation is just an exception. Mention runCatching by name; it's the most common real bug.

Follow-up questions

Think of your answer first, then open the hint.

  1. How would you write a safe runCatching for suspend code?

    Show hint

    Catch, then rethrow if the exception is a CancellationException.

  2. Is TimeoutCancellationException a CancellationException?

    Show hint

    Yes, so the same rule applies.

Suggest a fix if something here is wrong or out of date.