跪拜 Guibai
← All articles
Android

Stop Choosing Between coroutineScope and supervisorScope — Fix the Failure Model First

By 潜龙勿用之化骨龙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Android teams waste hours debating coroutineScope versus supervisorScope when the real fix is architectural: stop throwing business errors as exceptions. A Result-driven data layer eliminates the debate entirely and prevents the common bug where CancellationException gets swallowed and converted into a spurious business failure.

Summary

Kotlin's coroutineScope cancels sibling tasks when one child fails, while supervisorScope isolates failures. But this distinction only matters when business errors propagate as exceptions. Once failures are wrapped in a Result type at the repository boundary, the coroutine sees only a normal return value — no exception, no cancellation, no difference between the two scopes.

The deeper problem is that an exception-driven architecture forces try-catch into every ViewModel and muddles control flow with data flow. CancellationException must always propagate upward as a control signal; catching it accidentally converts a lifecycle event into a business failure, wasting resources and breaking cancellation.

A clean split puts technical exceptions in the Data layer, converts them to Result.failure, and lets the coroutine scope manage only lifecycle, concurrency, and cancellation. The ViewModel then consumes Result like any other data, without a single try-catch.

Takeaways
coroutineScope cancels all sibling coroutines when one child throws an uncaught exception; supervisorScope does not.
Neither scope automatically handles exceptions — uncaught exceptions still propagate to CoroutineExceptionHandler and crash the app.
An outer try-catch cannot catch exceptions thrown inside a child coroutine created with launch, because launch creates a new Job with its own propagation path.
Wrapping business failures in Result<T> makes them normal return values that the coroutine never sees as exceptions, so coroutineScope and supervisorScope behave identically.
CancellationException inherits from Exception and must be re-thrown in every catch block; catching it silently converts a cancellation signal into a Result.failure and keeps the coroutine running after the ViewModel is destroyed.
The Data layer should catch IOException and other technical exceptions and convert them to Result.failure; the ViewModel should never write try-catch for business errors.
Coroutine scopes should manage lifecycle, concurrency, and cancellation — not business error display or user messaging.
Conclusions

The entire coroutineScope-versus-supervisorScope debate is a symptom of a deeper design mistake: treating business failures as exceptions rather than as ordinary data. Fix the data layer, and the scope choice becomes a non-issue for business logic.

Kotlin's decision to make CancellationException a subclass of Exception is a recurring footgun. A catch-all `catch(e: Exception)` silently swallows cancellation, and the fix — a dedicated re-throw clause — is easy to forget until a production bug surfaces.

Result-driven architectures invert the typical Android error-handling pattern: instead of exceptions flowing upward and being caught at the UI, technical exceptions are caught at the lowest layer and converted into data that flows upward through normal return channels.

Concepts & terms
coroutineScope
A scope builder that creates a structured concurrency boundary where failure of any child coroutine cancels all other children and propagates the exception upward.
supervisorScope
A scope builder similar to coroutineScope, but a child's failure does not cancel sibling coroutines. The exception still propagates upward if uncaught.
CancellationException
A special exception in Kotlin coroutines that signals cooperative cancellation. It inherits from Exception, so a generic catch block will swallow it unless explicitly re-thrown. It must always propagate upward to properly cancel the coroutine.
Result<T>
A Kotlin standard library type representing either a successful value or a failure. When used as a return type in suspend functions, business failures become ordinary data that do not trigger coroutine exception propagation.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗