Explain the cause parameter of onCompletion. What are its possible values and how do you distinguish normal, cancelled, and failed completion?
answer
- cause == null => normal
- cause is CancellationException => cancelled
- else => failure
- Don't log CancellationException as error
- catch before onCompletion => cause null
basics
~10 sThe 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 sonCompletion'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 linesfun <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
Knows cause can be null or an exception and that onCompletion runs for every ending.
Cleanly separates null/CancellationException/other and avoids logging cancellation as an error.
Explains observational (non-consuming) semantics and how catch ordering rewrites the observed cause.
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