What does the 'flow invariant: emission from another coroutine is not allowed' rule mean, and how is the flow { } builder enforced to be sequential and context-preserving?
answer
- emit must be on the producer's own coroutine
- No emit inside launch/async/withContext => IllegalStateException
- Error: 'Flow invariant is violated'
- flowOn to change dispatcher; channelFlow/callbackFlow for concurrency
- Enforced by SafeCollector via Job/context comparison
basics
~10 sInside flow { } you must call emit from the same coroutine that runs the block. You can't launch a new coroutine and emit from there. If you need that, use channelFlow instead.
solid answer
~40 sThe flow { } builder enforces 'context preservation' and sequential emission. emit must be called from the exact coroutine (and coroutine context) that started the producer. You cannot wrap emit in a launch { }, withContext { }, or a different dispatcher and emit from there — at runtime the collector throws an IllegalStateException: 'Flow invariant is violated: Emission from another coroutine ... is detected.' This keeps emissions sequential and lets flowOn handle upstream context changes safely. The check is done by comparing the coroutine context's Job. If you genuinely need concurrent emission from multiple coroutines or a callback, use channelFlow { } (which provides send and a thread-safe channel) or callbackFlow { }. To change the dispatcher of the producer, use .flowOn(dispatcher) rather than withContext inside the block.
code
kotlin · 10 lines// Wrong: crashes at runtime
flow {
launch { emit(1) } // IllegalStateException: Flow invariant is violated
}.collect { }
// Right: concurrent emission needs channelFlow
channelFlow {
launch { send(1) }
launch { send(2) }
}.collect { println(it) }go deeper
May only know emit produces values; likely unaware of the invariant.
Knows you shouldn't emit from another coroutine and that flowOn changes dispatcher.
Explains context preservation, the IllegalStateException, and chooses flowOn vs channelFlow/callbackFlow correctly.
Reasons about why fail-fast was chosen, the SafeCollector mechanism, and the design trade-offs of channelFlow's buffering/concurrency vs plain flow's lockstep guarantees.
## The flow invariant `flow { }` guarantees two things: **context preservation** and **sequential emission**. Together these mean: every `emit` call must happen in the same coroutine that the builder's block started in, with the same coroutine context (specifically the same `Job`). Why? It makes flows composable and predictable: the collector always observes emissions one at a time, in order, and `flowOn` can rewrite the upstream context safely without races. ## What is NOT allowed ```kotlin // ILLEGAL — emit from a child coroutine val bad = flow { coroutineScope { launch { emit(1) // throws IllegalStateException at runtime } } } // ALSO ILLEGAL — emit inside withContext changes the context val bad2 = flow { withContext(Dispatchers.IO) { emit(1) // throws: Flow invariant is violated } } ``` The runtime detects this with `SafeCollector`, which checks that the context of the coroutine calling `emit` matches the collector's context. The error message is: `Flow invariant is violated: Emission from another coroutine is detected ... Please refer to 'flow' documentation or use 'flowsOn' instead.` ## The correct ways - **To change dispatcher of the producer:** use `.flowOn(Dispatchers.IO)`. It runs the upstream (including the `flow { }` block) on that dispatcher in a single coroutine, preserving the invariant. ```kotlin val good = flow { emit(loadFromDisk()) // suspend, runs on IO via flowOn below }.flowOn(Dispatchers.IO) ``` - **To emit concurrently / from callbacks:** use `channelFlow { }` or `callbackFlow { }`. These give you `send(value)` (or `trySend`) on a thread-safe channel, so multiple coroutines or a callback thread can produce values safely. ```kotlin val concurrent = channelFlow { launch { send(fetchA()) } launch { send(fetchB()) } } ``` ## Why it matters Violating the invariant in a plain `flow { }` is a bug that surfaces as a crash, not silent corruption — Kotlin chose fail-fast. Knowing the right alternative (`channelFlow`/`callbackFlow` vs `flowOn`) is the senior signal.
- Why is emitting from withContext(Dispatchers.IO) { } inside flow { } illegal, but .flowOn(Dispatchers.IO) is fine?withContext changes the emitting coroutine's context, breaking context preservation, so emit is now in a 'different' context. flowOn shifts the whole upstream onto its own coroutine on that dispatcher and bridges back, preserving the invariant.
- What does channelFlow give you that flow { } does not?A thread-safe channel with send/trySend that can be called from multiple coroutines or callbacks concurrently; it also lets the producer and collector run more decoupled.
saying these in an interview costs you the question
- Thinks you can emit from inside launch as long as you join it
- Suggests withContext around emit to switch dispatcher
- Unaware of channelFlow/callbackFlow as the concurrent alternative
- Believes the violation fails silently rather than throwing