Structured Concurrency & Cancellation Quiz

KOTLIN › Coroutines

Child A throws an IllegalStateException. What happens to child B?

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?

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?

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?

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?

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?

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?

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?

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?

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.

The block has not finished after 2 seconds. What happens?

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?

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.

When does the coroutineScope call return?

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?

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?

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.

What is wrong with a repository that creates its own CoroutineScope(Dispatchers.IO) internally?

Answer: The scope outlives the ViewModel, leaks coroutines after the screen is destroyed, and cannot be controlled in tests

A privately-owned scope has no lifecycle tie -- it outlives the ViewModel and leaks work. It also can't be swapped for a TestDispatcher. The fix: accept a dispatcher parameter, use withContext, and let the ViewModel own the scope.

In structured concurrency, what happens to child coroutines when their CoroutineScope is cancelled?

Answer: All child coroutines in that scope are cancelled too

Cancellation propagates down the coroutine hierarchy: cancelling a scope (or its Job) recursively cancels all of its child coroutines.

What distinguishes supervisorScope from coroutineScope?

Answer: In supervisorScope, one child's failure won't cancel its siblings

supervisorScope uses a SupervisorJob, so a child's failure is isolated and does not cancel its siblings, unlike coroutineScope where one failure cancels the whole scope.

A coroutine runs a long CPU loop with no suspension points and does not stop when its scope is cancelled. Why?

Answer: Cancellation is cooperative, so an unchecked loop never sees the cancelled state

Cancellation only sets a flag; cooperative code must hit a suspension point or check isActive/ensureActive/yield to actually react, otherwise a tight loop keeps running.

A coroutine is cancelled while inside a blocking call with no suspension points. What happens?

Answer: Nothing until the call returns: it stays Cancelling and holds its thread

Cancellation needs a suspension point or a flag check to take effect, and a blocking call offers neither. That is what runInterruptible and invokeOnCancellation exist to bridge.

Which tool converts coroutine cancellation into a thread interrupt for legacy blocking IO?

Answer: runInterruptible { }

runInterruptible interrupts the executing thread on cancellation, which blocking Java IO surfaces as an InterruptedException. NonCancellable does the opposite, and the other two only work where the code already checks or suspends.

Why use cancelAndJoin() rather than cancel() before starting a replacement job?

Answer: cancel() returns before the old coroutine has stopped, so both run briefly at once

The overlap is what produces duplicated requests in a search box. Joining first guarantees the previous coroutine has finished unwinding before the replacement starts.

What does yield() provide beyond a cancellation check?

Answer: It hands the dispatcher to other coroutines waiting to run

yield suspends and reschedules, so it is both a cancellation point and a fairness point. Checking isActive alone keeps you cancellable but can starve other coroutines on a single-threaded dispatcher.

Back to Structured Concurrency & Cancellation