Free Coroutines & Flow Interview Questions & Answers
Structured concurrency, cancellation, and hot vs cold Flow.
All 50 questions and detailed answers are free. No account or sign-in required.
What does the suspend keyword actually change about a function, and does it make the function run off the main thread?
suspend only marks that the function may suspend and resume without blocking a thread; the compiler rewrites it into a state machine that passes a Continuation. It does not switch threads or introduce any concurrency by itself, so a suspend function called on the main dispatcher still runs on the main thread. If you want to move work off the main thread you must explicitly change dispatcher, for example with withContext(Dispatchers.IO). A common bug is a suspend function that does blocking I/O yet is called on Main, freezing the UI despite the suspend marker.
In structured concurrency, what guarantee does a coroutineScope block give you that GlobalScope.launch does not?
coroutineScope suspends until all of its child coroutines complete, and if any child fails it cancels its siblings and rethrows, so the scope cannot outlive its children and errors are not lost. GlobalScope has no parent Job and no lifecycle tie, so launched coroutines can leak, keep running after the surrounding work is done, and swallow or crash on exceptions with no propagation path. Structured concurrency means every coroutine has a parent that waits for it and owns its cancellation, which GlobalScope deliberately breaks.
What is the difference between launch and async, and when is using async without ever calling await a mistake?
launch returns a Job and is fire-and-forget for side effects, while async returns a Deferred that carries a result and defers exposing exceptions until you call await. Using async purely for its concurrency but never awaiting is a mistake because a failure in that async will not surface where you expect, and if it is a root coroutine the exception can sit silently in the Deferred. If you do not need a return value, use launch so exceptions propagate through the normal parent channel.
Why does wrapping an async call site in try/catch sometimes fail to catch the exception?
async surfaces its exception through await, so the try/catch must be around the await call, not around the async that starts it. If you catch around async you only catch failures in starting the coroutine, not the work it does. Additionally, when async is a child of a normal Job, the exception also propagates to the parent immediately and can cancel the scope, so catching it at await may still leave the parent scope canceled unless you used supervisorScope. The correct pattern is to put the coroutine under a supervisor and try/catch the await.
What is the core behavioral difference between coroutineScope and supervisorScope?
In coroutineScope a failure in any child cancels all other children and the scope itself, treating the children as one atomic unit. In supervisorScope a child failure is isolated: it does not cancel siblings or the scope, and each child's failure is handled independently. Use supervisorScope when you launch several independent tasks and want one to fail without killing the rest, for example loading several UI sections. Note that supervisorScope still rethrows if you use async and await a failed child, because await re-raises.
How do Job and SupervisorJob differ in the direction that cancellation and failure propagate?
Cancellation always propagates downward in both: cancel a parent and all children are cancelled. The difference is upward failure propagation: with a regular Job, a failing child cancels the parent and thus all siblings, whereas with a SupervisorJob a child's failure stays with that child and does not cancel the parent or siblings. So SupervisorJob only changes how a child's failure moves up, not how cancellation moves down. Placing a SupervisorJob deep in the tree only shields the subtree beneath it.
A CoroutineExceptionHandler you installed does not fire for an exception thrown inside an async. Why?
CoroutineExceptionHandler only handles uncaught exceptions from launch coroutines that are roots of the hierarchy; it is invoked as a last resort. async exceptions are considered handled when you call await, so the handler is never consulted for them. It also has no effect if installed on a non-root coroutine, since the exception propagates to the root first. To handle an async failure you must catch it at await, and to use the handler you install it on the root scope's context of a launch.
Why is putting a CoroutineExceptionHandler in the context of a child launch, rather than the scope, ineffective?
An uncaught exception propagates up to the root coroutine before any handler runs, and only the handler present in the root coroutine's context is used. A handler installed on an inner child is ignored because by the time the exception is treated as uncaught it has already moved to the parent. The handler must be part of the CoroutineScope or the top-level launch context. This is why people are surprised their per-task handler never fires; only the outermost one matters.
What is the practical difference between withContext(Dispatchers.IO) and launching an async on the IO dispatcher and awaiting it immediately?
withContext switches the dispatcher for the enclosing block, suspends the current coroutine, runs the block, and returns the result sequentially without starting a new concurrent coroutine. async plus await starts a new child coroutine that can run concurrently with other work before you await it. If you immediately await, the two are functionally similar but withContext is cheaper and clearer because it expresses sequential context switching, while async is for actual parallel decomposition. Using async just to switch context is an anti-pattern.
What actually distinguishes Dispatchers.IO from Dispatchers.Default, and can work migrate between them cheaply?
Dispatchers.Default is sized to the number of CPU cores for CPU-bound work, while Dispatchers.IO is a larger, elastic pool (default 64 threads or more) tuned for blocking I/O where threads sit idle waiting. Crucially they share the same underlying thread pool, so switching from Default to IO with withContext can reuse the same thread without an actual thread handoff in many cases, making the transition cheap. Choose IO for blocking calls so you do not starve the CPU pool, and Default for heavy computation.
What does limitedParallelism on a dispatcher give you, and how does it differ from a fixed-size thread pool?
limitedParallelism creates a view over an existing dispatcher that caps how many coroutines run concurrently on it, without allocating new threads; it draws from the parent dispatcher's shared pool. This lets you, for example, restrict IO-bound access to a resource to N concurrent operations while still sharing the elastic IO threads. It differs from creating a dedicated fixed thread pool because there are no extra threads or lifecycle to close, and multiple limited views cooperate over the same underlying pool. It is the modern replacement for newFixedThreadPoolContext.
Why does Dispatchers.Unconfined behave surprisingly, and where is it legitimately used?
Unconfined starts the coroutine in the caller's thread but after the first suspension point resumes in whatever thread the resuming code uses, so the thread can change unpredictably mid-coroutine. This makes it unsuitable for general use because you lose control over threading. It is legitimately used in tests where you want eager, immediate execution without a dispatcher queue, and in a few advanced cases where you know no thread confinement is needed. In production code seeing Unconfined is usually a red flag.
Why does a tight computational loop ignore cancellation, and what are the ways to make it cooperative?
Cancellation in coroutines is cooperative: it sets the Job to cancelling and relies on the coroutine hitting a suspension point or a cancellation check to actually stop. A tight loop that never suspends and never checks never observes the cancelled state, so it runs to completion. You make it cooperative by checking isActive and breaking, calling ensureActive which throws CancellationException, or calling yield which both checks cancellation and lets other coroutines run. All suspending functions from kotlinx.coroutines already check cancellation, so pure computation is the usual offender.
What is the difference between checking isActive, calling ensureActive, and calling yield inside a loop?
isActive is a Boolean you test and then decide how to exit gracefully, so it will not throw. ensureActive throws CancellationException immediately if the coroutine is no longer active, giving you fail-fast cancellation without manual branching. yield additionally suspends to give other coroutines on the dispatcher a chance to run, and it also checks cancellation, so it is useful for fairness in long CPU loops. Choose ensureActive for simple correctness, and yield when you also want to avoid monopolizing the thread.
Why can't you simply run suspending cleanup code in a finally block after cancellation, and what fixes it?
Once a coroutine is cancelled its Job is in the cancelling state, and any suspending call in a finally block throws CancellationException immediately, so your cleanup never completes. To run suspending cleanup you must wrap it in withContext(NonCancellable), which provides a Job that ignores the cancellation so the suspend calls can finish. This is the sanctioned pattern for releasing resources, closing connections, or emitting a final state during cancellation. Keep NonCancellable blocks short so you do not defeat cancellation entirely.
What is a cold Flow, and what specifically makes it cold?
A cold Flow does nothing until it is collected; the block you pass to the flow builder runs fresh, from the start, each time a collector subscribes, and stops when collection stops. There is no shared emission and no state kept between collectors, so two collectors each trigger an independent execution. This is unlike hot streams like StateFlow or SharedFlow that emit regardless of collectors and share emissions. Coldness makes Flow lazy, cancellable, and backpressure-friendly, but means side effects in the builder happen per collector.
Why does StateFlow drop values, and how do its conflation and equality check affect what collectors see?
StateFlow is conflated: it only holds the latest value, so if you update it faster than a collector consumes, intermediate values are lost and the collector sees only the most recent. It also deduplicates using equals, meaning setting the same value by equality does not emit at all. This bites when you emit a data class that compares equal to the previous one, or when you rely on receiving every intermediate state; StateFlow guarantees the current value, not every transition. Use SharedFlow with sufficient buffer if you need every event.
You update a StateFlow with a value equal to the current one and collectors do not react. Why, and how do you force an emission?
StateFlow compares the new value to the current one with equals and suppresses emission when they are equal, to avoid redundant updates. If your type is a data class, equal contents mean no emission even if it is a different instance. To force distinct emissions you must make the values unequal, for example by including a changing field like a monotonic id or timestamp, or by using a type without value equality. This is by design and is why StateFlow is unsuitable for one-shot events that may repeat identically.
What do replay, extraBufferCapacity, and onBufferOverflow control in a MutableSharedFlow, and how do they interact?
replay is how many recent values a new collector immediately receives on subscription; extraBufferCapacity is additional buffer beyond replay for values not yet collected by slow collectors. onBufferOverflow decides what happens when both are full: SUSPEND makes the emitter suspend for backpressure, DROP_OLDEST discards the oldest buffered value, or DROP_LATEST discards the incoming one. If replay and extraBufferCapacity are both zero and there is no active collector, tryEmit fails and emit suspends. Tuning these lets you choose between never losing events and never blocking the producer.
Why does SharingStarted.WhileSubscribed(5000) exist, and what problem does the 5000 specifically solve?
WhileSubscribed starts the upstream flow when the first collector subscribes and stops it when the last unsubscribes, which conserves resources. The 5000 millisecond stop timeout keeps the upstream alive for five seconds after the last collector leaves, so a short-lived unsubscribe such as a configuration change or screen rotation does not tear down and immediately restart an expensive upstream. Without the timeout you would cancel and recreate the flow on every rotation, losing cached state and doing redundant work. It is the standard choice for UI state in ViewModels.
How do SharingStarted.Eagerly, Lazily, and WhileSubscribed differ in when the upstream runs?
Eagerly starts the upstream immediately when stateIn or shareIn is called and keeps it running for the scope's lifetime regardless of collectors, so it can waste work with no subscribers. Lazily starts on the first collector and then keeps running forever even after all collectors leave. WhileSubscribed only runs while there is at least one collector, optionally with a stop timeout, and can also drop the replay cache after a timeout. For lifecycle-aware UI state you almost always want WhileSubscribed so the upstream pauses when the screen is not visible.
What is the difference between stateIn and shareIn, and when must you use shareIn?
stateIn converts a cold flow into a StateFlow that always has a current value, so it requires an initial value and conflates. shareIn converts a cold flow into a SharedFlow that shares emissions among collectors with a configurable replay, and it does not require an initial value nor guarantee a current value. Use shareIn when you have events rather than state, or when you need replay semantics other than the single-latest-value behavior of StateFlow. Both take a scope and a SharingStarted policy to control upstream lifetime.
What exactly does flowOn change, and why does it only affect the upstream part of the chain?
flowOn changes the CoroutineContext, typically the dispatcher, for the operators that appear before it in the chain, upstream of the flowOn call. It works because Flow preserves context downstream: the collector's context governs the terminal collect, and flowOn creates a boundary that runs upstream emissions on the specified dispatcher and channels them back to the collector. This is why flowOn is directional and why placing it near the end changes almost everything above it. You can use multiple flowOn calls to give different upstream segments different dispatchers.
Why is calling withContext to change dispatcher inside a flow builder's emit wrong, and what is context preservation?
Flow enforces context preservation: a value must be emitted from the same coroutine context in which the flow builder runs, so emitting from inside a withContext block that changes the dispatcher throws an IllegalStateException about the flow being invoked in a different context. The correct way to move upstream work to another dispatcher is the flowOn operator, which safely relocates emissions across a channel boundary. This rule exists so exception transparency and cancellation behave predictably. In short, never wrap emit in withContext; use flowOn.
What is the difference between buffer, conflate, and collectLatest for handling a slow collector?
buffer inserts a channel so the producer can keep emitting into a buffer while the collector works, decoupling their speeds without dropping values until the buffer policy dictates. conflate is like buffer with a capacity that keeps only the latest value, so a slow collector skips intermediate emissions. collectLatest cancels the previous collector block when a new value arrives and restarts it with the newest value, so in-progress processing of stale values is abandoned. Choose conflate to skip stale values you never started, and collectLatest to abandon stale values you already started processing.
How does flatMapLatest behave differently from flatMapMerge and flatMapConcat?
flatMapLatest cancels the previously started inner flow whenever a new upstream value arrives and switches to the new inner flow, ideal for search-as-you-type where only the latest query matters. flatMapConcat processes inner flows sequentially, fully collecting one before starting the next, preserving order but serializing. flatMapMerge runs multiple inner flows concurrently up to a concurrency limit and interleaves their emissions, maximizing throughput but not preserving order. The trap is using flatMapMerge where you needed cancellation of stale work, causing outdated results to arrive.
Why is callbackFlow required for bridging callback-based APIs, and what happens if you forget awaitClose?
callbackFlow provides a coroutine-safe channel-backed builder where you can call send or trySend from external callbacks that fire on other threads, which the plain flow builder forbids. awaitClose is mandatory to suspend the builder until the flow is cancelled so you can unregister the callback; forgetting it makes the builder complete immediately or throw, tearing down the flow and leaking the still-registered callback. awaitClose is your one chance to remove listeners, so it must always unregister the underlying source. This prevents leaks when the collector stops.
What does suspendCancellableCoroutine give you over suspendCoroutine, and why does it matter?
suspendCancellableCoroutine returns a CancellableContinuation that respects coroutine cancellation, so if the coroutine is cancelled while awaiting the callback, the continuation is cancelled and you can register invokeOnCancellation to clean up, for example cancelling the underlying request. suspendCoroutine ignores cancellation, so a cancelled coroutine would still wait for the callback and leak. For any bridge to an async API you should use the cancellable variant and wire cancellation to abort the underlying operation. Also resume the continuation exactly once, or you get an IllegalStateException.
Why is runBlocking dangerous in production Android code, and where is it acceptable?
runBlocking blocks the calling thread until its coroutine completes, so calling it on the main thread freezes the UI and can cause ANRs, defeating the entire point of coroutines. It also blocks a valuable pool thread if used on a dispatcher. It is acceptable in main functions of command-line tools, in unit tests as a bridge though runTest is preferred for suspend tests, and occasionally at the very edge of a non-coroutine framework callback that must block. Inside app code you should suspend or launch, never runBlocking.
Why does GlobalScope.launch leak and misbehave, beyond just being discouraged?
GlobalScope has a lifetime of the whole application and no parent, so coroutines launched in it are never cancelled when the screen, ViewModel, or request that started them goes away, leaking memory and continuing to touch dead objects. It also breaks structured concurrency, so exceptions have no parent to propagate to and callers cannot wait for completion. This leads to work that survives navigation, duplicated in-flight requests, and crashes from updating destroyed UI. Use a lifecycle-bound scope like viewModelScope or a coroutineScope instead.
What lifecycle does viewModelScope have, and what dispatcher does it default to?
viewModelScope is tied to the ViewModel and is cancelled automatically when the ViewModel is cleared in onCleared, so coroutines started in it do not outlive the ViewModel. It uses a SupervisorJob so one failing child does not cancel the others, and it defaults to Dispatchers.Main.immediate so UI updates run without an unnecessary re-dispatch when already on the main thread. This makes it the correct home for UI-driving coroutines, while you still switch to IO or Default for heavy work with withContext inside. You should not manually cancel it.
Why is repeatOnLifecycle preferred over launchWhenStarted for collecting flows in the UI?
launchWhenStarted merely suspends the coroutine when below the started state but keeps the coroutine and its upstream alive, so an upstream flow can keep producing and holding resources while the UI is in the background. repeatOnLifecycle actually cancels the coroutine block when the lifecycle drops below the target state and restarts it fresh when it returns, so the upstream collection is truly stopped and resources released while backgrounded. This prevents wasted work and hidden updates to invisible UI, which is why the launchWhenX APIs are deprecated in favor of repeatOnLifecycle.
When collecting a flow with repeatOnLifecycle(Lifecycle.State.STARTED), what happens to a StateFlow's upstream on each stop and start?
Each time the lifecycle drops below STARTED the collecting coroutine is cancelled, so if the StateFlow was created with WhileSubscribed and the last collector leaves, the upstream stops after its timeout; when the lifecycle returns to STARTED a new collector subscribes and the upstream restarts. Because StateFlow retains its latest value, the UI immediately receives that cached current state on resubscription without redoing work if within the WhileSubscribed timeout window. This pairing of repeatOnLifecycle with WhileSubscribed(5000) is the canonical efficient pattern. It avoids both leaks and redundant restarts across rotations.
What is exception transparency in Flow, and why is wrapping emit in try/catch a violation?
Exception transparency means a flow must not catch exceptions thrown downstream by collectors or later operators; it should only handle exceptions from its own upstream. Wrapping emit in a try/catch can swallow a downstream failure, breaking this contract and hiding errors, which is why the framework may throw to enforce it. The correct way to handle upstream errors is the catch operator, which only intercepts exceptions from upstream and never from the collector. Keep your producer logic free of broad try/catch around emit and use catch declaratively.
How does the catch operator differ from a try/catch around collect, and what can catch not handle?
The catch operator is placed in the flow chain and only catches exceptions that originate upstream of it, leaving downstream and collector exceptions untouched, and it can emit a fallback value or complete gracefully. A try/catch around collect catches everything including exceptions thrown inside the collector block itself, but it cannot let the flow continue emitting afterward. So catch cannot handle an exception thrown in your collect lambda, and try/catch around collect cannot recover the flow mid-stream. Choosing between them depends on whether the error is in production or consumption.
What is the semantic difference between combine and zip on two flows?
zip pairs emissions one-to-one, waiting for both flows to emit before producing a combined value, and completes when either flow completes, so it is for lockstep pairing. combine emits a new combined value whenever either flow emits, using the latest value from the other, so it reflects the most recent state of both. The trap is using zip for state where the two flows emit at different rates; you will stall waiting for pairs and lose responsiveness. Use combine for reactive state composition and zip for correlating positionally matched items.
In combine, why might you miss the very first combined emission, and how does it handle initial values?
combine does not emit until every input flow has produced at least one value, because it needs a latest value from each to form the tuple. If one flow is slow to emit its first value, or never emits, combine produces nothing, which surprises people expecting an immediate result. To guarantee an initial emission you can give each source a starting value with onStart or use StateFlow inputs that always have a current value. So combining a StateFlow with a flow that has not emitted yet still waits for that flow's first value.
Why is runTest preferred over runBlocking for testing suspend code, and what does it automate?
runTest runs on a TestScope with a virtual clock and a TestDispatcher, so delays are skipped by advancing virtual time rather than really waiting, making tests fast and deterministic. It also automatically waits for scheduled coroutines to finish and fails on uncaught exceptions or leaked coroutines. runBlocking would actually sleep through delays and does not give you time control or leak detection. runTest additionally lets you control ordering via the scheduler, which is essential for testing concurrency and timeouts without flakiness.
What is the difference between StandardTestDispatcher and UnconfinedTestDispatcher in tests?
StandardTestDispatcher queues coroutines and does not run them until you advance the scheduler with advanceUntilIdle or runCurrent, giving you precise control over interleaving and letting you assert intermediate states. UnconfinedTestDispatcher starts coroutines eagerly and runs them as far as possible immediately, which is convenient when you just want things to execute without manual advancing but hides ordering. Choose Standard when order matters or you test suspension points, and Unconfined for simple tests where eager execution is fine. Mixing them incorrectly is a common cause of tests that pass or hang unexpectedly.
What does advanceUntilIdle do, and why might a test not observe an emission without it?
advanceUntilIdle runs the virtual clock forward executing all queued and delayed coroutines until nothing is left scheduled, so pending launches and delays complete. With a StandardTestDispatcher coroutines are queued but not executed until you advance, so if you assert immediately after launching you see nothing because the coroutine has not run yet. Calling advanceUntilIdle, or runCurrent for only currently ready tasks, flushes that work so your assertions observe the results. Forgetting it is the classic reason a StateFlow value looks unchanged in a test.
Why should you inject dispatchers rather than reference Dispatchers.IO directly, especially for tests?
Hardcoding Dispatchers.IO or Default makes code run on real background threads that you cannot control or fast-forward in tests, causing flakiness and forcing real waits. Injecting a dispatcher, often via a small provider interface, lets you substitute a TestDispatcher whose virtual clock you advance deterministically, and lets you set the Main dispatcher with Dispatchers.setMain. This is the standard testability pattern and also decouples business logic from concrete threading choices. Without it, unit tests of coroutine code become slow and nondeterministic.
When should you use Mutex.withLock instead of a synchronized block in coroutine code?
synchronized blocks the thread while waiting for the monitor, which defeats coroutines because a suspended coroutine can resume on a different thread and because blocking ties up pool threads; you also cannot call suspend functions inside synchronized safely. Mutex is a suspending, non-blocking lock: withLock suspends the coroutine rather than blocking the thread if the lock is held, and it is safe to await inside the critical section. Use Mutex for coordinating access across coroutines, but keep critical sections short, and prefer confining mutable state to a single coroutine or using atomics when possible.
Mutex in kotlinx.coroutines is not reentrant. What problem does that cause and how do you avoid it?
Because Mutex is not reentrant, a coroutine that holds the lock and then calls another function that tries to acquire the same lock will deadlock itself, waiting forever for a lock it already owns. This differs from a reentrant Java lock or synchronized which the same thread can re-enter. Avoid it by not acquiring the same Mutex twice in a call chain: restructure so the lock is taken at one level and the inner function assumes it is already held, or split the state so nested locking is unnecessary. Careful lock scoping prevents self-deadlock.
When would you choose a Channel over a SharedFlow, given both can broadcast events?
A Channel delivers each element to exactly one receiver and is a hot, one-shot conduit ideal for producer-consumer handoff or work distribution, and it applies backpressure via suspension. SharedFlow broadcasts each value to all active collectors and can replay, making it right for events multiple observers should see. Use a Channel when each event must be processed once, such as a task queue, and SharedFlow when you fan out to many subscribers such as UI events. Note that a Channel buffers and holds an undelivered event when there is no receiver, unlike a zero-replay SharedFlow which would drop it.
Why can a SharedFlow with zero replay silently drop events for one-time UI actions, and what is a robust alternative?
A MutableSharedFlow with replay zero and no buffer delivers an emitted value only to collectors active at that instant; if the UI is not currently collecting, for example during a configuration change, the event is lost with no replay to recover it. This is a frequent source of missed navigation or toast events. A robust approach is to use a Channel consumed as receiveAsFlow, which buffers the event until a collector consumes it exactly once, or to model the event as state that is explicitly consumed. The key is guaranteeing at-least-once delivery for critical one-shot actions.
Why does cancelling a parent coroutine not immediately stop a child doing blocking JVM work, and how do you make it interruptible?
Cancellation only flips the Job state and relies on suspension points to throw; a child stuck in a blocking JVM call like Thread.sleep or a synchronous socket read has no suspension point, so it keeps running until the blocking call returns. You make it responsive either by using a truly suspending API, by periodically calling ensureActive around chunks of work, or by using runInterruptible which maps coroutine cancellation to thread interruption so an interruptible blocking call throws InterruptedException. Simply wrapping blocking code in withContext(Dispatchers.IO) does not make it cancellable.
Why must you never catch CancellationException and swallow it, and what is the correct handling?
CancellationException is the mechanism by which structured concurrency signals a coroutine to stop; catching it and not rethrowing makes the coroutine appear to complete normally, breaking cancellation and potentially causing parent scopes to wait or resources to leak. If you have a broad try/catch, you should rethrow CancellationException or catch more specific exceptions, and cleanup should go in finally with NonCancellable if it suspends. Modern helpers like currentCoroutineContext().ensureActive() or rethrowing in the catch preserve correct behavior. Swallowing it is one of the most common and subtle coroutine bugs.
How do onEach plus launchIn differ from a plain collect, and when does the distinction matter?
collect is a suspending terminal operator that runs in the current coroutine and blocks that coroutine's progress until the flow completes. onEach defines a side effect per element and launchIn starts collection in the given scope as a separate coroutine, returning a Job, so it does not suspend the calling code and lets you collect multiple flows concurrently. The distinction matters when you want to start several collections without each blocking the next, or when you want a Job to cancel the collection independently. Using collect where you needed launchIn can serialize flows you intended to run together.
What is the difference between a SupervisorJob installed in a scope and using supervisorScope, in terms of lifetime?
A SupervisorJob placed in a long-lived CoroutineScope, like viewModelScope, gives that whole scope supervisor semantics for the scope's entire lifetime, so all children are independently supervised for as long as the scope lives. supervisorScope is a temporary, block-scoped supervisor that suspends until its children finish and then returns, applying supervision only for that block. Use the scope-level SupervisorJob for a durable component like a ViewModel, and supervisorScope for a bounded unit of work where you want a few concurrent tasks isolated then joined. They solve the same isolation need at different lifetimes.
Why can awaiting an async inside supervisorScope still throw even though a child failure does not cancel the scope, and how do you handle it correctly?
supervisorScope prevents a failing child from cancelling siblings and the scope, but await re-raises the failed Deferred's exception at the call site because await's contract is to return the result or throw the failure. So the isolation applies to propagation through the Job hierarchy, not to the value you explicitly request via await. Handle it by wrapping each await in its own try/catch so one failed async yields a fallback while the others still succeed. This combination, supervisorScope plus per-await try/catch, is how you run several independent async tasks and tolerate partial failure.
Practice all Coroutines & Flow questions interactively
Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.
More Android interview topics
- Kotlin interview questions
- Jetpack Compose interview questions
- Android Architecture interview questions
- Android Framework interview questions
- Testing interview questions
- Dependency Injection interview questions
- Networking interview questions
- Room & Persistence interview questions
- Android System Design interview questions
- Android Tools interview questions