Structured Concurrency & Cancellation Interview Questions

KOTLIN › Concurrency

Why does Kotlin make cancellation cooperative rather than forcibly terminating a coroutine's thread the way some other concurrency systems kill a task? What trade-off does that design choice introduce?

What a strong answer covers: Forcibly killing a thread mid-execution can leave shared state, resources, or files half-updated with no chance to clean up, so Kotlin instead flips an isActive flag and relies on suspension points and checks like ensureActive() to notice and unwind gracefully, which lets finally blocks and resource cleanup run safely. The trade-off is that a coroutine doing tight CPU work with no suspension points and no manual isActive check simply won't respond to cancellation at all, which is a common source of bugs and a much-loved interview trap.

How would you go about finding and fixing a coroutine that leaks, one that keeps running long after the screen that launched it is gone?

What a strong answer covers: In practice you look for coroutines started on the wrong scope, GlobalScope, a manually created CoroutineScope that's never cancelled, or a scope captured before a screen's lifecycle owner rather than tied to it, and you verify by checking Job.isActive/isCancelled in logs or using StrictMode/leak detection during QA. The fix is almost always ensuring the coroutine's scope is structurally tied to the component's lifecycle, viewModelScope, lifecycleScope with repeatOnLifecycle, so cancellation is automatic rather than something you have to remember to do manually.

You need to fetch three independent pieces of data for a screen: one is critical and must block rendering if it fails, the other two are optional and should just hide their own section on failure. How do you structure the coroutine scopes to get that behavior, and what happens if you get it wrong?

What a strong answer covers: The critical fetch runs inside the normal coroutineScope so its failure propagates and cancels the siblings and fails the whole screen load, while the two optional fetches are wrapped individually, either with their own try/catch around each async's await, or launched inside a supervisorScope so one failing doesn't cancel the others or the critical path. Getting it backwards, putting everything in one coroutineScope, means a failure in an optional section takes down the entire screen instead of just hiding a card, which is the exact bug this design question is probing for.

Back to Structured Concurrency & Cancellation