Flow Context, Errors & Backpressure Quiz
KOTLIN › Flow
What does context preservation mean for a collect lambda?
- It runs in the context that called collect, whatever the flow does upstream
- It always runs on Dispatchers.Main, regardless of which context actually called it
- It runs on the dispatcher of the last operator before it
- It runs on the dispatcher the flow builder used
Answer: It runs in the context that called collect, whatever the flow does upstream
The collector owns its context and the flow may not change it, which is why a ViewModel collecting on Main needs no withContext around the lambda.
What produces IllegalStateException: Flow invariant is violated?
- Calling emit from a different context than the builder is own, typically inside withContext
- Collecting the same cold flow from two entirely different coroutines at the very same time
- Placing catch above retry in the operator chain
- Emitting more values than the buffer capacity allows
Answer: Calling emit from a different context than the builder is own, typically inside withContext
Emission must happen in the builder context so the collector context guarantee holds. The fix is flowOn, or a channel-backed builder if you truly must emit from elsewhere.
Which dispatcher does map run on here?
- Dispatchers.Default, claimed by the flowOn below it
- Dispatchers.IO, inherited from the flowOn above it
- Dispatchers.Main, since flowOn affects only the builder
- Whichever thread the previous emission finished on
Answer: Dispatchers.Default, claimed by the flowOn below it
Each flowOn claims what is above it and not already claimed, so reading bottom-up: Default claims map, and IO claims the builder.
Why does adding flowOn also change buffering behaviour?
- It inserts a channel to cross the thread boundary, so the upstream can run ahead
- It disables emit suspension entirely, for the whole length of the operator chain
- It converts the cold flow into a hot one with a replay cache
- It runs the producer eagerly before collection starts
Answer: It inserts a channel to cross the thread boundary, so the upstream can run ahead
Crossing dispatchers requires a channel, and that channel is a buffer. So flowOn implies buffer, which is why timing can change when you add it.
An exception thrown inside a collect { } lambda is not caught by a catch placed last. Why?
- catch handles upstream failures only, never the collector is own
- catch must be paired with retry before it takes effect
- The collector runs on a different dispatcher, outside the catch scope
- catch only matches IOException unless given a predicate
Answer: catch handles upstream failures only, never the collector is own
Upstream-only is deliberate: it prevents catch from silently swallowing bugs in your own collector. Use an ordinary try/catch around the collect call.
What does retry(3) re-run on a cold flow?
- The entire upstream from scratch, repeating any producer side effects
- Only the operator that actually threw, leaving all the earlier stages intact
- Only the failed emission, replayed from an internal cache
- Nothing: it merely suppresses the exception three times
Answer: The entire upstream from scratch, repeating any producer side effects
Retry re-collects the whole upstream, which for a cold flow means running the producer again. That is fine for an idempotent network read and a problem for a producer with side effects.
How does Flow implement backpressure?
- emit suspends until the collector is ready, so the producer cannot run ahead
- The collector calls request(n) to signal how many values it is able to accept next
- An internal queue drops the oldest values once it reaches capacity
- The producer is sampled at an interval matched to collector speed
Answer: emit suspends until the collector is ready, so the producer cannot run ahead
Suspension is the entire protocol, which is why a plain chain neither drops values nor queues without bound. buffer is the explicit opt-out when you want overlap.
What is conflate() equivalent to?
- buffer(1, BufferOverflow.DROP_OLDEST)
- buffer(1, BufferOverflow.SUSPEND)
- collectLatest applied as an intermediate operator
- distinctUntilChanged() with a capacity of one
Answer: buffer(1, BufferOverflow.DROP_OLDEST)
That equivalence is why a plain buffer placed after conflate fuses into one configuration and silently removes the conflation.
What buffering does this chain actually have?
- A single 50-element suspending buffer: the conflation is fused away
- Conflation feeding a 50-element queue
- Two independent stages applied in order
- A runtime error, raised by the two conflicting buffer configurations
Answer: A single 50-element suspending buffer: the conflation is fused away
Adjacent buffer-family operators fuse into one channel configuration and the later call wins. To get both, ask for buffer(50, BufferOverflow.DROP_OLDEST) explicitly.
How do conflate() and collectLatest differ?
- conflate drops values but lets the collector finish; collectLatest cancels the collector mid-work
- conflate cancels the producer outright, whereas collectLatest cancels the collector block
- conflate suspends the producer; collectLatest drops values
- They behave identically, differing only in placement in the chain
Answer: conflate drops values but lets the collector finish; collectLatest cancels the collector mid-work
The interruption is the distinction. collectLatest suits expensive cancellable collector work such as an in-flight request for a search query.
Why might (1..1000).asFlow().collect { } ignore cancellation?
- It never suspends, so nothing checks for cancellation without cancellable()
- asFlow produces a hot flow, which cannot be cancelled
- Terminal operators always run inside NonCancellable
- The range is evaluated eagerly, before the collection has even properly started
Answer: It never suspends, so nothing checks for cancellation without cancellable()
The free check lives inside the flow builder emit. A non-suspending source has no such point, so you add cancellable() or check ensureActive() yourself.
When does onCompletion run?
- On every ending: normal completion, failure, or cancellation
- Only when the flow completes without an exception
- Only when the flow fails, as a lighter alternative to using catch
- After every emission, once the collector lambda returns
Answer: On every ending: normal completion, failure, or cancellation
Its cause parameter is null on success and non-null otherwise, which makes it the right place for cleanup like hiding a spinner regardless of outcome.
Where should catch sit relative to onStart if it must also cover the loading emission?
- Below onStart, since catch only sees operators above it
- Above onStart, so it is installed before anything emits
- Either position works: catch covers the whole chain
- Adjacent to collect, because only terminal position is valid
Answer: Below onStart, since catch only sees operators above it
Field of view is strictly upstream, so anything you want covered has to be above the catch. Putting it last covers the whole chain except the collector.
Which builder should you use when values arrive from a callback that cannot suspend?
- callbackFlow, with trySend and an explicit BufferOverflow policy
- flow { }, wrapping the callback registration inside a withContext
- flowOf, passing the values once they have all arrived
- asFlow, applied to the callback listener object
Answer: callbackFlow, with trySend and an explicit BufferOverflow policy
A callback cannot suspend to satisfy backpressure, so this is the one case where you make the drop-or-suspend decision explicitly. flow { } would throw the invariant violation.
What is operator fusion?
- Adjacent flowOn, buffer and conflate merging into one channel configuration
- Two flows being combined into one by zip or combine
- The compiler inlining operator lambdas to avoid allocation
- Consecutive map calls being collapsed down into one single combined transform
Answer: Adjacent flowOn, buffer and conflate merging into one channel configuration
These operators are the ones people stack, so the library merges them rather than allocating a channel each. The visible consequence is that a later buffer overrides an earlier conflate.
Which statement about the catch operator is correct?
- It catches exceptions thrown inside the collect { } lambda
- It catches exceptions from operators placed below it in the chain
- It only catches exceptions emitted by the upstream flow above it
- It silently swallows all exceptions and cannot re-emit values
Answer: It only catches exceptions emitted by the upstream flow above it
catch is restricted to upstream exceptions. It cannot see the collect lambda or downstream operators, and inside it you may rethrow or emit a fallback.
A slow collector processes a fast producer, but you only ever need the most recent value and want to drop the ones in between. Which operator do you use?
- buffer
- conflate
- retry
- distinctUntilChanged
Answer: conflate
conflate keeps only the latest value, skipping intermediates the collector couldn't keep up with. buffer would queue all of them; distinctUntilChanged only removes consecutive duplicates.
Using collectLatest, what happens when a new value arrives before the previous collector block has finished processing?
- The new value is queued and each block runs strictly in order
- The new value is dropped and the running block keeps going
- The running block is cancelled and restarted with the new value
- Both blocks keep running at the same time on separate dispatchers
Answer: The running block is cancelled and restarted with the new value
collectLatest cancels the in-progress collector block whenever a newer value is emitted and relaunches it with that value, so only the processing of the latest value runs to completion.