Structured Concurrency & Cancellation Quiz
KOTLIN › Concurrency
Inside a coroutineScope you launch two children A and B. A throws an IllegalStateException. What happens to B?
- B keeps running to completion, unaffected by A's exception
- B is cancelled because the scope fails and cancels siblings
- B is merely paused until A is restarted and finishes cleanly
- The exception is swallowed, so B and the scope keep going
Answer: B is cancelled because the scope fails and cancels siblings
coroutineScope uses a regular Job, so an uncaught child failure cancels the scope and all sibling coroutines, then rethrows from the scope.
You want each child coroutine to fail independently without cancelling its siblings. Which construct do you use?
- withContext(Dispatchers.Default) for switching dispatchers
- coroutineScope for structured child completion and cleanup
- supervisorScope or a SupervisorJob-backed scope
- runBlocking when you need to bridge suspend code to a caller
Answer: supervisorScope or a SupervisorJob-backed scope
supervisorScope / SupervisorJob make cancellation one-directional, so a child's failure does not cancel its parent or siblings.
A coroutine runs a tight CPU loop with no suspension points. You call job.cancel(). What happens?
- The loop keeps running, as cancellation is cooperative and unchecked
- The loop stops instantly at the next line of code after cancel()
- A CancellationException is thrown immediately in the middle of work
- The runtime force-kills the thread, so the loop cannot continue running
Answer: The loop keeps running, as cancellation is cooperative and unchecked
Cancellation only sets a flag; code that never suspends or checks isActive/ensureActive()/yield() keeps running despite cancel().
Which statement about ensureActive() is correct?
- It returns a Boolean indicating whether the job is currently active
- It suspends the current coroutine briefly to let other coroutines run
- It throws CancellationException at once if the job is cancelled
- It permanently disables cancellation for the enclosing block of code
Answer: It throws CancellationException at once if the job is cancelled
ensureActive() is fail-fast: it throws CancellationException right away when cancelled, unlike isActive (a boolean) or yield (which also suspends).
In a launch block you write catch (e: Exception) { log(e) } around suspending work and do not rethrow. What is the main bug?
- It means exceptions can no longer be logged anywhere in the block
- It catches CancellationException, so cancellation is ignored
- It forces the coroutine to run on the main thread by default
- It turns the coroutine into an async task that returns Deferred
Answer: It catches CancellationException, so cancellation is ignored
Catching the broad Exception also catches CancellationException; without rethrowing it, the coroutine ignores cancellation. Catch specific types like IOException instead.
Where does a CoroutineExceptionHandler have NO effect?
- On a root launch coroutine whose scope context directly contains the handler
- As a generic handler for any uncaught failure in a launched root coroutine
- On a child launch inside supervisorScope that installs the handler itself
- On an async coroutine, where the exception is kept in the Deferred
Answer: On an async coroutine, where the exception is kept in the Deferred
async never routes to a CoroutineExceptionHandler; its exception is held in the Deferred and rethrown when you call await(). The handler works for uncaught launch failures at the root.
You must run resource-cleanup code in a finally block, but it contains a suspend call that would be skipped because the coroutine is already cancelled. What do you use?
- withContext(NonCancellable) { ... } around the cleanup code
- GlobalScope.launch { ... } to run the cleanup in a new coroutine
- yield() before the cleanup to give cancellation a chance to clear
- withTimeout(0) { ... } to force the cleanup to finish immediately
Answer: withContext(NonCancellable) { ... } around the cleanup code
withContext(NonCancellable) lets suspending cleanup run to completion even in an already-cancelled coroutine; it is intended for finally blocks, not for launching new work.
Inside a coroutineScope you write async { repo.load() } but never call await() on the resulting Deferred, and load() throws. When does the failure surface?
- It is silently swallowed forever, because await() is simply never called here
- It is routed straight to a CoroutineExceptionHandler that the scope installed
- It propagates to the parent at once and cancels the scope; non-root async reports up
- It is held in the Deferred and thrown only if some later code eventually calls await()
Answer: It propagates to the parent at once and cancels the scope; non-root async reports up
async only defers its exception until await() when it is a root coroutine; as a child of coroutineScope an uncaught failure cancels the parent immediately regardless of await().
A ViewModel launches network work in viewModelScope. The user navigates away and the ViewModel is cleared. What happens to that in-flight coroutine?
- It is cancelled automatically when viewModelScope is cleared.
- It keeps running and must be cancelled manually in onCleared().
- It is moved to GlobalScope so it survives the ViewModel cleanup.
- It is paused and later resumed in a new ViewModel of the same type.
Answer: It is cancelled automatically when viewModelScope is cleared.
viewModelScope is tied to the ViewModel lifecycle and is cancelled in onCleared(), so its child coroutines are cancelled automatically without manual cleanup.
You wrap a suspend call in withTimeout(2000) { ... } and the block has not finished after 2 seconds. What happens?
- It returns null and lets the block keep running in the background.
- It throws TimeoutCancellationException and cancels the coroutine.
- It blocks the thread until completion and ignores the timeout limit.
- It returns any partial result the block may have computed so far.
Answer: It throws TimeoutCancellationException and cancels the coroutine.
withTimeout throws TimeoutCancellationException (a CancellationException subclass) and cancels the block; use withTimeoutOrNull if you want a null result instead of an exception.
Why is launching app work in GlobalScope discouraged in Android code?
- It always runs its coroutines on the main thread, blocking the entire UI
- It cannot call suspend functions from within its coroutine body
- It automatically cancels its work whenever any Activity gets destroyed
- It has no lifecycle, so its coroutines aren't cancelled by any parent
Answer: It has no lifecycle, so its coroutines aren't cancelled by any parent
GlobalScope has no parent Job or lifecycle, so it breaks structured concurrency: work is not cancelled when the caller goes away, risking leaks and orphaned coroutines.
Inside a suspend function you write coroutineScope { launch { delay(1000); save() } } with no await(). When does the coroutineScope call return?
- Only after the launched child finishes; the scope awaits its children.
- Immediately, because launch is fire-and-forget and the scope ignores it.
- After a fixed 100ms grace period, no matter when the child actually ends.
- Never, because launch detaches the child and the scope cannot finish.
Answer: Only after the launched child finishes; the scope awaits its children.
Structured concurrency guarantees a scope suspends until every child (including fire-and-forget launch) completes, so coroutineScope returns only after the child finishes.
What does supervisorScope's failure isolation actually apply to?
- Every coroutine anywhere in its tree, even many levels below the scope
- Only async children, while launch children are excluded from failure rules
- Only its direct children; a nested non-supervisor scope still fails normally
- Nothing; it works exactly like coroutineScope with no failure isolation
Answer: Only its direct children; a nested non-supervisor scope still fails normally
supervisorScope only changes propagation for its immediate children; a regular coroutineScope nested inside a child still uses a normal Job, so failures there propagate and fail that child as usual.
Which of these fetches A and B sequentially instead of in parallel?
- val a = async { loadA() }; val b = async { loadB() }; a.await() + b.await()
- coroutineScope { val a = async { loadA() }; val b = async { loadB() }; a.await() + b.await() }
- listOf(async { loadA() }, async { loadB() }).awaitAll()
- val a = async { loadA() }.await(); val b = async { loadB() }.await()
Answer: val a = async { loadA() }.await(); val b = async { loadB() }.await()
Calling await() on the same line that starts each async forces loadA() to finish before loadB() even begins; true parallelism requires starting both async coroutines first, then awaiting.