skip to content

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%

answer

  1. emit must stay in the collector's context
  2. withContext around emit -> IllegalStateException
  3. Work in withContext, emit outside it
  4. flowOn relocates the whole upstream
  5. Concurrent producers -> channelFlow + send

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.

solid answer

~40 s

emit is not thread/context-safe across coroutine boundaries: Flow enforces that all emissions come from the same context the builder block started in (the collector's context). Wrapping emit in withContext(Dispatchers.IO) or calling emit from a child launch/async runs it in a different CoroutineContext, so Flow throws IllegalStateException: 'Flow invariant is violated... Emission from another coroutine context'. To move producer work to another dispatcher, do the work in withContext but emit outside it, or apply flowOn(Dispatchers.IO) to the whole upstream. If you genuinely need to emit from multiple coroutines concurrently, use the channelFlow { } builder with send (which is concurrency-safe), not flow { } with emit.

code

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

fun load() = 42

fun bad() = flow {
    withContext(Dispatchers.IO) { emit(load()) }  // throws IllegalStateException
}

fun goodWithContext() = flow {
    val data = withContext(Dispatchers.IO) { load() } // work off-thread
    emit(data)                                        // emit stays in context
}

fun goodFlowOn() = flow { emit(load()) }.flowOn(Dispatchers.IO)

fun goodConcurrent() = channelFlow {
    launch { send(load()) }   // send is concurrency-safe
}

go deeper

for a junior

Recognizes that emit from a different context throws and that flowOn is the usual fix.

for a middle

Explains why the check exists (backpressure/cancellation) and gives the work-vs-emit pattern.

for a senior

Distinguishes flow/emit's single-context rule from channelFlow/send concurrency and picks the right tool.

for a principal

Reasons about the invariant as protecting structured concurrency and designs APIs that don't tempt context violations.

## The rule being enforced `flow { }` plus `emit` is designed for a **single, sequential** producer that runs in the **collector's context**. The library actively checks this at runtime to keep flows predictable and to keep structured-concurrency/cancellation intact. ## What triggers the exception Any `emit` that executes in a context different from where the builder block was entered: ```kotlin // 1) withContext around emit flow { withContext(Dispatchers.IO) { emit(load()) } // throws } // 2) emit from a launched child coroutine flow { coroutineScope { launch { emit(1) } // throws: different coroutine } } ``` Both throw **`IllegalStateException`** with a message like: *"Flow invariant is violated: Emission from another coroutine context, but it should be the same context..."* ## Why it must throw - `emit` participates in **backpressure** and **cancellation** tied to the collector's coroutine. Emitting from a foreign coroutine would corrupt that coordination. - It would silently break **context preservation**, the guarantee downstream relies on. ## Correct fixes **A) Do work off-thread, emit on-thread:** ```kotlin flow { val data = withContext(Dispatchers.IO) { load() } // work moves emit(data) // emit stays put } ``` **B) Relocate the whole upstream with flowOn:** ```kotlin flow { emit(load()) }.flowOn(Dispatchers.IO) ``` **C) Concurrent emission → channelFlow:** ```kotlin channelFlow { launch { send(loadA()) } // send IS concurrency-safe launch { send(loadB()) } } ``` `channelFlow` builds the flow around a `Channel`, so multiple coroutines may `send` safely; it lifts the single-context restriction of `flow {}`/`emit`. ## Key APIs/keywords - `flow { }`, `emit`, `withContext`, `launch`, `coroutineScope` - `IllegalStateException` (the violation error) - `flowOn` (legal upstream context change) - `channelFlow { }` + `send` (concurrent-safe emission)

  • Is it ever fine to call withContext inside a flow builder?
    Yes — as long as emit is NOT inside it. Use withContext to do the heavy work, then emit the result outside the withContext block.
  • What builder lets multiple coroutines emit concurrently?
    channelFlow {} with send. It is backed by a Channel and explicitly supports concurrent emission from launched coroutines.

emit is like signing a form that must be signed at one specific desk; stepping to another desk to sign it invalidates the form.

saying these in an interview costs you the question

  • Wrapping emit in withContext and expecting it to work
  • Calling emit from a launched coroutine inside flow {}
  • Catching the IllegalStateException and ignoring it instead of restructuring
  • Not knowing channelFlow exists for concurrent emission

context