I'm moving away from Threads and trying to implement Coroutines for a data-intensive app. I keep running into issues with structured concurrency and Job cancellation. What is the best way to handle exceptions in a CoroutineScope so that one failing child doesn't kill the whole parent process?
3 answers
Structured concurrency is key. You should generally avoid using GlobalScope as it lives for the lifetime of the app and can cause leaks. Use CoroutineScope tied to a lifecycle. For error handling, use SupervisorJob or supervisorScope. Unlike a regular Job, a SupervisorJob allows a child coroutine to fail without cancelling its siblings or the parent. Also, always use try-catch inside the coroutine or a CoroutineExceptionHandler. Remember that cancellation is cooperative—your code must check isActive or call suspending functions like yield() to be cancellable.
How are you testing these Coroutines? I've found that debugging race conditions in Coroutines can be even trickier than with standard threads if you aren't using the right Dispatchers.
I always recommend using Flow for continuous data streams. It’s built on top of Coroutines and handles backpressure naturally, which is great for data-heavy apps.
Susan is right. Switching to Flow for our database queries and API polling made our state management so much more predictable compared to standard suspend functions.
Mark, testing is definitely a challenge. We use runTest from the kotlinx-coroutines-test library. It provides a TestDispatcher that lets you control "virtual time," which makes it much easier to test delays and timeouts without actually waiting. It really helps in identifying those tricky race conditions by making the execution deterministic during your CI runs.