Structured Concurrency & Cancellation Quiz

KOTLIN › Concurrency

Inside a coroutineScope you launch two children A and B. A throws an IllegalStateException. What happens to 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.

You wrap a suspend call in withTimeout(2000) { ... } and 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.

Inside a suspend function you write coroutineScope { launch { delay(1000); save() } } with no await(). 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.

Back to Structured Concurrency & Cancellation