skip to content

onStart / onCompletion

onStart emits before collection begins — handy for a loading state — and onCompletion runs on success, cancellation, or failure with the cause available. Together they are how you attach state transitions without wrapping the collect in a try/finally.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What do the Flow operators onStart and onCompletion do, and when does each block run?

level: juniorimportance: must knowfreq 70%

answer

  1. onStart = before first emit
  2. onCompletion = after end, always
  3. cause: null / cancel / failure
  4. Both can emit() values
  5. onCompletion observes, doesn't catch

basics

~10 s

onStart runs once right before a flow starts producing values. onCompletion runs once after the flow finishes, whether it ended normally, was cancelled, or threw an error.

solid answer

~40 s

Both are intermediate operators on Flow. onStart { } executes its block in the collector's context just before collection begins and can even emit() extra values first. onCompletion { cause -> } executes after the upstream finishes for any reason; its nullable Throwable parameter is null on success, the CancellationException on cancellation, or the failure exception otherwise. onCompletion can also emit() a final value. Neither catches exceptions: onCompletion only observes the cause and rethrows it, so you still need catch or a try/finally around collect for handling. They are commonly used to toggle loading spinners: onStart shows the spinner, onCompletion hides it regardless of outcome.

code

kotlin · 7 lines
kotlin
flow {
    emit(1)
    emit(2)
}
    .onStart { emit(0) }                       // prepends 0
    .onCompletion { cause -> println("end: $cause") } // cause == null here
    .collect { println(it) }                   // prints 0,1,2 then "end: null"

go deeper

for a junior

Knows onStart runs before, onCompletion runs after for any outcome, and uses them for spinners.

for a middle

Adds that both run in the collector context, can emit(), and that onCompletion's cause distinguishes success/cancel/failure.

for a senior

Stresses onCompletion observes-not-catches semantics and pairs it correctly with catch ordering.

for a principal

Frames them as declarative lifecycle hooks vs try/finally and reasons about context preservation and exception transparency guarantees.

## What is a Flow A `Flow<T>` is Kotlin's cold asynchronous stream: nothing runs until a terminal operator such as `collect` is called. `onStart` and `onCompletion` are *intermediate* operators (they return a new `Flow` and don't trigger execution themselves) used to hook into the start and end of that collection. ## onStart ```kotlin flowOf(1, 2, 3) .onStart { println("about to start") } .collect { println(it) } ``` - The block runs **once**, **before** the upstream flow emits its first value, in the **collector's coroutine context**. - The lambda has an `FlowCollector<T>` receiver, so you may call `emit()` inside it to prepend values: `.onStart { emit(0) }` emits `0` before the upstream values. - Typical use: show a loading indicator, log, or seed an initial value. ## onCompletion ```kotlin flowOf(1, 2, 3) .onCompletion { cause -> println("done, cause=$cause") } .collect { println(it) } ``` - The block runs **once**, **after** the flow stops producing values, no matter how it stopped. - The single parameter `cause: Throwable?` tells you *why* it ended: - `null` — the flow completed **normally**. - a `CancellationException` — the collecting coroutine was **cancelled**. - any other `Throwable` — the flow (or downstream) **failed** with that exception. - Like `onStart`, the lambda is a `FlowCollector<T>`, so you can `emit()` a final value (only safe if the cause is null/non-fatal). ## Key rule: onCompletion does NOT swallow exceptions `onCompletion` only **observes** the terminating cause and then **rethrows** it. It is like a `finally` block, not a `catch`. To actually handle the exception you still need the `catch` operator upstream of completion, or a `try/finally` around `collect`. ```kotlin flow { emit(1); throw RuntimeException("boom") } .onCompletion { cause -> println("cleanup, cause=$cause") } // prints boom, then rethrows .catch { e -> println("handled $e") } // place catch AFTER onCompletion to handle it .collect() ``` ## Why prefer these over try/finally - They keep lifecycle logic in the operator chain, attach to the **specific flow**, and run in the collector context, making spinner-toggle code symmetric and declarative.

  • Can onStart emit values, and where do they appear in the stream?
    Yes. Its lambda is a FlowCollector, so emit() inside onStart prepends those values before any upstream emissions reach the collector.
  • If a flow throws, does the onCompletion block stop the exception from propagating?
    No. onCompletion sees the cause but rethrows it; you still need catch or try/finally to handle the error.

onStart/onCompletion are the 'lights on' and 'lights off' switches that flank a stage performance, no matter how the show ends.

saying these in an interview costs you the question

  • Saying onCompletion catches/handles the exception
  • Thinking onStart runs after the first emission
  • Claiming onCompletion only runs on success
  • Confusing onCompletion's cause with the emitted value
  • Believing these operators trigger collection on their own

context

open as a page

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%

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.

open as a page

Show how onStart and onCompletion are used to drive a loading/idle UI state, and why this is preferred over try/finally around collect.

level: middleimportance: should knowfreq 55%

basics

~10 s

Use onStart to switch the UI to loading before data arrives, and onCompletion to switch it back to done after the flow ends. They keep show/hide symmetric and tied to that one flow.

open as a page

Contrast onCompletion with the catch operator and with a try/finally inside a flow { } builder. Which can handle exceptions, which only observe, and which run in the collector's context?

level: seniorimportance: should knowfreq 45%

basics

~10 s

catch handles and stops upstream errors. onCompletion only watches the ending and rethrows. A try/finally inside flow { } runs in the producer and cleans up there. Use onCompletion to observe, catch to recover.

open as a page

What are the subtle pitfalls of onStart and onCompletion: emitting after failure, exception transparency violations, and ordering with flowOn and catch?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

Don't emit from onCompletion after a failure, don't treat onCompletion as a place to swallow errors, and remember catch and flowOn ordering changes what onCompletion sees and which thread it runs on.

open as a page