Cancellation is cooperative: cancel() flags a coroutine, and its next suspension point throws CancellationException, so finally blocks run. A failing child cancels its siblings (Coroutines vs Promises) unless the scope has a SupervisorJob, as viewModelScope does; a CoroutineExceptionHandler gets the rest.
import kotlinx.coroutines.*
fun main() = runBlocking {
val sync = launch {
try { repeat(10) { log("syncing page $it"); delay(100) } }
finally { log("finally: closing the connection") }
}
delay(250); sync.cancelAndJoin()
val handler = CoroutineExceptionHandler { _, e -> log("handler caught ${e.message}") }
val screen = CoroutineScope(SupervisorJob() + handler) // like viewModelScope
screen.launch { delay(50); error("cover download failed") }
screen.launch { delay(100); log("prices loaded anyway") }.join()
}Output
0 ms [main @coroutine#2] syncing page 0 135 ms [main @coroutine#2] syncing page 1 238 ms [main @coroutine#2] syncing page 2 365 ms [main @coroutine#2] finally: closing the connection 430 ms [DefaultDispatcher-worker-1 @coroutine#3] handler caught cover download failed 480 ms [DefaultDispatcher-worker-1 @coroutine#4] prices loaded anyway
Pitfalls: a CPU loop without suspension points ignores cancellation (call ensureActive()); catch (e: Exception) swallows CancellationException; and a failed async throws at await() but also cancels its parent scope.