Posts Tagged ‘Cancellation’
[KotlinConf2019] Kotlin Coroutines: Mastering Cancellation and Exceptions with Florina Muntenescu & Manuel Vivo
Kotlin coroutines have revolutionized asynchronous programming on Android and other platforms, offering a way to write non-blocking code in a sequential style. However, as Florina Muntenescu and Manuel Vivo, both prominent Android Developer Experts then at Google, pointed out at KotlinConf 2019, the “happy path” is only part of the story. Their talk, “Coroutines! Gotta catch ’em all!” delved into the critical aspects of coroutine cancellation and exception handling, providing developers with the knowledge to build robust and resilient asynchronous applications.
Florina and Manuel highlighted a common scenario: coroutines work perfectly until an error occurs, a timeout is reached, or a coroutine needs to be cancelled. Understanding how to manage these situations—where to handle errors, how different scopes affect error propagation, and the impact of launch
vs. async
—is crucial for a good user experience and stable application behavior.
Structured Concurrency and Scope Management
A fundamental concept in Kotlin coroutines is structured concurrency, which ensures that coroutines operate within a defined scope, tying their lifecycle to that scope. Florina Muntenescu and Manuel Vivo emphasized the importance of choosing the right CoroutineScope
for different situations. The scope dictates how coroutines are managed, particularly concerning cancellation and how exceptions are propagated.
They discussed:
* CoroutineScope
: The basic building block for managing coroutines.
* Job
and SupervisorJob
: A Job
in a coroutine’s context is responsible for its lifecycle. A key distinction is how they handle failures of child coroutines. A standard Job
will cancel all its children and itself if one child fails. In contrast, a SupervisorJob
allows a child coroutine to fail without cancelling its siblings or the supervisor job itself. This is critical for UI components or services where one failed task shouldn’t bring down unrelated operations. The advice often given is to use SupervisorJob
when you want to isolate failures among children.
* Scope Hierarchy: How scopes can be nested and how cancellation or failure in one part of the hierarchy affects others. Understanding this is key to preventing unintended cancellations or unhandled exceptions.
Cancellation: Graceful Termination of Coroutines
Effective cancellation is vital for resource management and preventing memory leaks, especially in UI applications where operations might become irrelevant if the user navigates away. Florina and Manuel would have covered how coroutines support cooperative cancellation. This means that suspending functions in the kotlinx.coroutines
library are generally cancellable; they check for cancellation requests and throw a CancellationException
when one is detected.
Key points regarding cancellation included:
* Calling job.cancel()
initiates the cancellation of a coroutine and its children.
* Coroutines must cooperate with cancellation by periodically checking isActive
or using cancellable suspending functions. CPU-bound work in a loop that doesn’t check for cancellation might not stop as expected.
* CancellationException
is considered a normal way for a coroutine to complete due to cancellation and is typically not logged as an unhandled error by default exception handlers.
Exception Handling: Catching Them All
Handling exceptions correctly in asynchronous code can be tricky. Florina and Manuel’s talk aimed to clarify how exceptions propagate in coroutines and how they can be caught.
They covered:
* launch
vs. async
:
* With launch
, exceptions are treated like uncaught exceptions in a thread—they propagate up the job hierarchy. If not handled, they can crash the application (depending on the root scope’s context and CoroutineExceptionHandler
).
* With async
, exceptions are deferred. They are stored within the Deferred
result and are only thrown when await()
is called on that Deferred
. This means if await()
is never called, the exception might go unnoticed unless explicitly handled.
* CoroutineExceptionHandler
: This context element can be installed in a CoroutineScope
to act as a global handler for uncaught exceptions within coroutines started by launch
in that scope. It allows for centralized error logging or recovery logic. They showed examples of how and where to install this handler effectively, for example, in the root coroutine or as a direct child of a SupervisorJob
to catch exceptions from its children.
* try-catch blocks: Standard try-catch blocks can be used within a coroutine to handle exceptions locally, just like in synchronous code. This is often the preferred way to handle expected exceptions related to specific operations.
The speakers stressed that uncaught exceptions will always propagate, so it’s crucial to “catch ’em all” to avoid unexpected behavior or crashes. Their presentation aimed to provide clear patterns and best practices to ensure that developers could confidently manage both cancellation and exceptions, leading to more robust and user-friendly Kotlin applications.