skip to content

Context Preservation & flowOn

Flow enforces context preservation: emit must happen in the collector's coroutine, so you change the upstream dispatcher with flowOn rather than withContext inside the builder. Doing it the wrong way throws at runtime, which is exactly the scenario interviewers describe.

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

questions

6

In Kotlin Flow, what does 'context preservation' mean, and in which coroutine context does emit run by default?

level: juniorimportance: must knowfreq 70%

answer

  1. emit runs in the collector's context
  2. Producer can't switch threads on its own
  3. No withContext around emit
  4. Use flowOn to change upstream
  5. Violation throws IllegalStateException

basics

~10 s

It means the code that produces values runs in the same place that collects them. By default, emit runs in the collector's coroutine, so the flow uses whatever context the collector started in.

solid answer

~30 s

Context preservation is the Flow guarantee that a flow's emissions happen in the same CoroutineContext as the collector. When you call collect from a coroutine, the body of flow { ... } and every emit run inside that collector's coroutine and its context (its CoroutineDispatcher, Job, etc.). The producer doesn't get to silently switch threads. This makes flows predictable: side effects in the builder run where you collect. To run upstream work on a different dispatcher you must use the dedicated flowOn operator rather than wrapping emit in withContext, which would break preservation and throw an IllegalStateException at runtime.

code

kotlin · 13 lines
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val f = flow {
        println("emit on ${Thread.currentThread().name}")
        emit(1)
    }
    withContext(Dispatchers.Default) {
        f.collect { println("collect on ${Thread.currentThread().name}") }
    }
    // emit and collect both report a Default-dispatcher thread
}

go deeper

for a junior

Knows emit runs in the collector's context and that flows don't switch threads by themselves.

for a middle

Explains the invariant, knows withContext around emit is illegal, and names flowOn as the fix.

for a senior

Connects context preservation to structured concurrency, cancellation, and predictable side effects.

for a principal

Frames preservation as a design contract enabling local reasoning and safe composition across teams/modules.

## What 'context preservation' means A **CoroutineContext** is the bundle of data a coroutine runs with — most importantly its **CoroutineDispatcher** (which thread/threadpool runs the code) and its **Job**. *Context preservation* is a core Flow rule: the values a flow emits are produced in **the same context in which the flow is collected**. Concretely, when you write: ```kotlin val f = flow { emit(1) // runs in the COLLECTOR's context emit(2) } withContext(Dispatchers.Default) { f.collect { println(it) } // collected on Default... } // ...so emit also runs on Default ``` the `flow { ... }` block and each `emit` execute inside the collector's coroutine. The producer is **not** allowed to change the context on its own. ## Why this matters - **Predictability:** any side effect or `emit` happens where you collect — no hidden thread hops. - **Structured concurrency:** the flow shares the collector's `Job`, so cancellation propagates correctly. - **Safety:** UI frameworks can rely on a flow collected on the main dispatcher staying on the main thread. ## The wrong way to switch threads Do NOT wrap `emit` in `withContext`: ```kotlin flow { withContext(Dispatchers.IO) { emit(load()) // ILLEGAL: emit from a different context } } ``` This violates preservation. Flow detects it and throws **`IllegalStateException`** with a message like "Flow invariant is violated: emission from another coroutine context". ## The right way: flowOn Use the **`flowOn(dispatcher)`** operator to change the **upstream** context without breaking the invariant (covered in depth in other questions). `flowOn` changes where producer code runs while still delivering emissions correctly to the collector. ## Key APIs/keywords - `flow { }` builder and `emit` (a `suspend` function) - `collect` (terminal, `suspend`) - `CoroutineContext`, `CoroutineDispatcher`, `Dispatchers.IO/Default/Main` - `flowOn` for legal upstream context change - `withContext` — legal in general, but NOT around `emit`

  • Is emit a suspend function?
    Yes. emit is a suspend function; it can only be called from the flow builder's suspending block and cooperates with backpressure and cancellation.
  • If you collect on Dispatchers.Main, where does the flow {} body run?
    On Dispatchers.Main — the collector's context. The producer code shares that dispatcher unless you add flowOn.

It's like a kitchen rule: cooks must plate the dish at the same counter where the waiter picks it up — they can't quietly move to another counter mid-service.

saying these in an interview costs you the question

  • Claiming the flow builder always runs on a background thread by default
  • Saying you should use withContext(IO) around emit to do IO
  • Thinking emit can be called from any coroutine you like
  • Confusing collect's context with the flow's declaration site

context

open as a page

Why does emitting from a different coroutine context (e.g. inside withContext or from launch) throw, and what is the correct fix?

level: middleimportance: must knowfreq 65%

basics

~20 s

Flow requires every emit to happen in the collector's context. If you emit from inside withContext or a launched coroutine, you've changed the context, so Flow throws an error. The fix is to use flowOn or channelFlow.

open as a page

What exactly does flowOn(dispatcher) change in a Flow pipeline, and why is it described as affecting only the 'upstream'?

level: middleimportance: must knowfreq 75%

basics

~10 s

flowOn changes the context (e.g. the dispatcher) for the operators and the producer that come before it in the chain. Everything after it, including collect, keeps the collector's context.

open as a page

How does flowOn affect buffering, concurrency, and emission ordering across the dispatcher boundary?

level: seniorimportance: should knowfreq 45%

basics

~20 s

flowOn runs the producer on another dispatcher and passes items to the collector through a channel. This lets producer and collector run at the same time, but items still arrive in the order they were emitted.

open as a page

Compare flowOn, collecting inside withContext, and launchIn for controlling where flow code runs. When is each correct?

level: seniorimportance: should knowfreq 40%

basics

~10 s

flowOn sets the context for the producer side. Collecting inside withContext sets the context for the collector and everything downstream. launchIn starts collection in a scope. They solve different problems and are often combined.

open as a page

Why did Kotlin's Flow API choose to enforce context preservation as an invariant rather than letting producers switch context freely (as RxJava does)? What does this buy and cost?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Enforcing that emit stays in the collector's context makes flows predictable and keeps cancellation and structured concurrency correct. The cost is you must use a dedicated operator (flowOn) to change threads instead of doing it inline.

open as a page