skip to content

Explain the cause parameter of onCompletion. What are its possible values and how do you distinguish normal, cancelled, and failed completion?

level: middleimportance: should knowfreq 60%

answer

  1. cause == null => normal
  2. cause is CancellationException => cancelled
  3. else => failure
  4. Don't log CancellationException as error
  5. catch before onCompletion => cause null

basics

~10 s

The cause is a nullable error. It is null when the flow finished fine, a cancellation error when the collector was cancelled, and the thrown exception when the flow failed.

solid answer

~40 s

onCompletion's lambda receives `cause: Throwable?`. `null` means the upstream completed normally. A non-null value is the exception that terminated the flow: a `CancellationException` (often `JobCancellationException`) when the collecting coroutine was cancelled, or any other `Throwable` when the flow failed. To distinguish them, check `cause == null` for success and `cause is CancellationException` for cancellation; everything else is a real failure. A common pattern logs errors only for genuine failures: `if (cause != null && cause !is CancellationException) log(cause)`. Note onCompletion does not consume the exception — after the block it propagates, so the cause you see is the same one collect throws unless an upstream catch handled it.

code

kotlin · 8 lines
kotlin
fun <T> Flow<T>.logLifecycle(tag: String): Flow<T> =
    onCompletion { cause ->
        when {
            cause == null -> println("$tag: ok")
            cause is CancellationException -> println("$tag: cancelled")
            else -> println("$tag: failed: ${cause.message}")
        }
    }

go deeper

for a junior

Knows cause can be null or an exception and that onCompletion runs for every ending.

for a middle

Cleanly separates null/CancellationException/other and avoids logging cancellation as an error.

for a senior

Explains observational (non-consuming) semantics and how catch ordering rewrites the observed cause.

for a principal

Connects cause handling to structured-concurrency cancellation contracts and designs reusable lifecycle operators around it.

## The signature ```kotlin fun <T> Flow<T>.onCompletion( action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit ): Flow<T> ``` The `cause` parameter is a `Throwable?` describing **why** collection ended. ## The three outcomes | Outcome | `cause` value | How to detect | |---|---|---| | Normal completion | `null` | `cause == null` | | Cancellation | a `CancellationException` (e.g. `JobCancellationException`) | `cause is CancellationException` | | Failure | the thrown `Throwable` (e.g. `IOException`) | `cause != null && cause !is CancellationException` | ## Why cancellation looks like a special exception In structured concurrency, cancelling a coroutine throws a `CancellationException` to unwind it. Flow surfaces that same exception as the completion `cause`. Treating cancellation as an *error* (e.g. logging it, showing an error toast) is a classic bug, so you almost always special-case it. ```kotlin upstream .onCompletion { cause -> when { cause == null -> log("completed") cause is CancellationException -> log("cancelled") // usually ignore else -> reportError(cause) // real failure } } .collect { /* ... */ } ``` ## onCompletion does not swallow the cause The `cause` you receive is **observational**. After your block runs, the same exception continues to propagate to the collector unless an upstream `catch` already handled it. So if you place `catch` **before** `onCompletion`, a handled failure will show up as `cause == null` in onCompletion (because catch turned the failure into normal completion). Operator ordering matters. ```kotlin flow { throw RuntimeException("x") } .catch { /* handled */ } // failure becomes normal completion .onCompletion { cause -> /* cause == null here */ } .collect() ``` ## Emitting from onCompletion based on cause Because the receiver is a `FlowCollector<T>`, you can emit a fallback value on success, but emitting after a failure is risky because the exception still propagates afterward. Guard with `if (cause == null) emit(default)`.

  • Why shouldn't you treat a CancellationException cause as an error?
    Cancellation is normal cooperative shutdown in structured concurrency; logging it or showing an error misreports expected teardown.
  • If you put catch before onCompletion and the flow failed, what cause does onCompletion see?
    null — catch converted the failure into normal completion, so onCompletion observes a successful end.

saying these in an interview costs you the question

  • Saying cause is the last emitted value
  • Treating cancellation as a failure to report
  • Claiming onCompletion consumes/handles the exception
  • Ignoring that operator ordering changes the observed cause
  • Assuming cause is always non-null when the flow stops

context