skip to content

Terminal Operators

Terminal operators are the suspending ones that actually run the flow: collect, toList, first, reduce, count, and launchIn for collecting in a scope without suspending. Nothing happens until one of them is called.

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

questions

5

What is a terminal operator on a Kotlin Flow, and why must you call one for any code to run? Give two examples.

level: juniorimportance: must knowfreq 82%

answer

  1. Cold + lazy: nothing runs until a terminal subscribes
  2. collect is the root; others build on it
  3. Terminals suspend (except launchIn)
  4. Each collect re-runs the whole pipeline
  5. toList / first / reduce / count are terminals

basics

~10 s

A terminal operator is the function that actually starts a Flow running and consumes its values. Without one, nothing happens because Flows are lazy. Examples: collect and toList.

solid answer

~40 s

Flows are cold and lazy: building one with flow { }, flowOf(), or chaining map/filter does nothing on its own. A terminal operator is a suspend function that triggers collection — it subscribes, runs the upstream code, and consumes the emitted values. collect { } is the most fundamental; others (toList, toSet, first, single, reduce, fold, count) are built on top of it. Because terminal operators suspend, they must be called from a coroutine (a suspend function or a launch/async block). Each terminal call re-runs the whole pipeline from scratch, since cold Flows produce a fresh stream per collector. launchIn(scope) is a non-suspending variant that starts collection in the given CoroutineScope.

code

kotlin · 9 lines
kotlin
val pipeline = flowOf(1, 2, 3, 4)
    .map { it * 10 }      // intermediate, lazy
    .filter { it > 15 }   // intermediate, lazy
// no work yet

suspend fun run() {
    val result = pipeline.toList()  // TERMINAL: triggers everything
    println(result)                 // [20, 30, 40]
}

go deeper

for a junior

Knows nothing runs until a terminal like collect/toList is called.

for a middle

Explains cold/lazy semantics and that collect underlies the others; knows terminals suspend.

for a senior

Articulates per-collector re-execution and contrasts cold Flow with hot StateFlow/SharedFlow.

for a principal

Frames terminal choice as an architectural decision (back-pressure, cancellation, resource lifetime) and when launchIn vs collect belongs in a layer.

## What 'terminal' means A Kotlin `Flow<T>` pipeline has two kinds of operations: - **Intermediate operators** (`map`, `filter`, `transform`, `onEach`, `take`...) — they return a new `Flow` and are *cold*: they only describe work and run nothing. - **Terminal operators** — they actually *start* the flow, run the upstream producer, and consume values. They are the only way to get results out. Because Flows are **cold** (lazy), the builder block (`flow { emit(...) }`) and every intermediate operator stay dormant until a terminal operator subscribes. This is the single most important mental model for Flow. ## collect — the foundation `collect` is a **suspend** function. Every other terminal operator is implemented in terms of it. ```kotlin val f = flow { println("producing") // does NOT run until collected emit(1); emit(2); emit(3) } // nothing printed yet f.collect { value -> println(value) } // now "producing", 1, 2, 3 ``` Because `collect` suspends, you can only call it inside a coroutine — a `suspend` function or a `launch { }`/`async { }`/`runBlocking { }` block. ## Common terminal operators - `collect { }` — run a lambda for each element. - `toList()` / `toSet()` — gather all elements into a `List`/`Set`. - `first()` / `first { predicate }` — take the first (matching) element, then cancel upstream. - `single()` — expect exactly one element; throws otherwise. - `count()` — number of elements. - `reduce { acc, v -> ... }` / `fold(initial) { acc, v -> ... }` — accumulate to a single value. - `launchIn(scope)` — the one **non-suspending** terminal; it launches collection in a `CoroutineScope` and returns a `Job`. ## Why each terminal re-runs the pipeline A cold Flow has no shared state; collecting it twice executes the producer twice: ```kotlin val nums = flow { emit(Random.nextInt()) } println(nums.first()) // one random number println(nums.first()) // a DIFFERENT random number — fresh execution ``` This is unlike `StateFlow`/`SharedFlow` (hot streams), where collectors share emissions. ## Key takeaway No terminal operator = no execution. Pick the terminal that matches what you need (a side effect, a collection, a single value, or an aggregate).

  • Can you call collect outside a coroutine?
    No. collect is a suspend function, so it must be invoked from another suspend function or a coroutine builder like launch/runBlocking.
  • Name a terminal operator that does NOT suspend.
    launchIn(scope) — it launches collection in a CoroutineScope and immediately returns a Job instead of suspending.

A Flow chain is a recipe; the terminal operator is finally turning on the stove — until then nothing cooks.

saying these in an interview costs you the question

  • Thinking map/filter execute code by themselves
  • Saying collect is optional / the flow runs when built
  • Confusing cold Flow with a hot StateFlow that shares emissions
  • Believing two collect calls share one execution
  • Calling collect from a plain (non-suspend) function

context

open as a page

Compare first(), first { predicate }, and single() as terminal operators. What does each do with the upstream flow, and when does each throw?

level: middleimportance: should knowfreq 60%

basics

~10 s

first() returns the first value and stops the flow. first { } returns the first matching value. single() expects exactly one value and throws if there are zero or more than one.

open as a page

Explain reduce and fold as terminal operators on Flow. How do they differ, and what happens with an empty flow?

level: middleimportance: should knowfreq 48%

basics

~10 s

Both combine all emitted values into one result. fold starts from a value you provide; reduce starts from the first emitted value. reduce throws on an empty flow; fold returns the initial value.

open as a page

What does launchIn(scope) do, and how does it differ from calling collect? When would you prefer it, and what are the lifecycle implications?

level: seniorimportance: should knowfreq 55%

basics

~20 s

launchIn(scope) starts collecting a flow in the background inside the given scope and returns a Job, instead of suspending the current code like collect does. It's used for fire-and-forget collection tied to a scope's lifecycle.

open as a page

When you call toList() on a flow, on which coroutine/dispatcher does the upstream producer run, and what happens to collection if the surrounding coroutine is cancelled mid-stream?

level: seniorimportance: nice to knowfreq 34%

basics

~10 s

By default the producer runs in the same context as the collector that called toList(). If that coroutine is cancelled while collecting, collection stops with a CancellationException and toList() does not return.

open as a page