Advanced Exception Handling in Kotlin Coroutines: A Guide for Android Developers
Mastering Exception Handling in Kotlin Coroutines: Handling Failures Like a Pro

Introduction
Exception handling in Kotlin Coroutines is often misunderstood, especially when dealing with structured concurrency, exception propagation, and parallel execution. A poorly handled coroutine failure can crash your Android app or lead to silent failures, making debugging difficult.
In this article, we will cover advanced scenarios of exception handling, including:
✅ How exceptions propagate in coroutine hierarchies
✅ Handling exceptions in async
, launch
, and supervisorScope
✅ Managing errors in Flow
, SharedFlow
, and StateFlow
✅ Retrying failed operations with exponential backoff
✅ Best practices for handling exceptions in ViewModel and WorkManager
By the end of this guide, you’ll be confidently handling coroutine failures in any Android project. 🚀
1. Exception Propagation in Structured Concurrency
Kotlin coroutines follow structured concurrency, meaning that when a parent coroutine is canceled, all of its children are also canceled. Likewise, when a child coroutine fails, the failure propagates up the hierarchy, canceling the entire coroutine scope.
Example: How a Failure in a Child Cancels the Entire Scope
val scope = CoroutineScope(Job())
scope.launch {
launch {
delay(500)
throw IllegalArgumentException("Child coroutine failed")
}
delay(1000)
println("This line will never execute")
}
⏳ What happens?
- The child coroutine throws an exception.
- The parent scope is canceled, and no other coroutines in the scope continue execution.
Solution: Use supervisorScope
to Prevent Propagation
To prevent failures from canceling all coroutines, wrap them inside a supervisorScope
:
scope.launch {
supervisorScope {
launch {
delay(500)
throw IllegalArgumentException("Child coroutine failed")
}
delay(1000)
println("This line will still execute")
}
}
📌 Key Takeaway: Use supervisorScope
when you want sibling coroutines to run independently, even if one fails.
2. async
vs launch
: Handling Exceptions Differently
How launch
and async
Handle Exceptions
launch {}
immediately cancels the parent scope if an exception is thrown.async {}
delays exception propagation untilawait()
is called.
Example: launch
Cancels Everything on Failure
scope.launch {
launch {
throw IOException("Network error")
}
delay(1000) // This will never execute
}
Example: async
Hides the Exception Until await()
val deferred = scope.async {
throw NullPointerException("Async failed")
}
deferred.await() // Exception is thrown here
🚨 Danger: If you forget to call await()
, the exception is silently ignored.
Solution: Wrap await()
Calls in Try-Catch
try {
val result = deferred.await()
} catch (e: Exception) {
Log.e("Coroutine", "Handled exception: $e")
}
📌 Key Takeaway: Always wrap await()
in a try-catch
block to prevent unhandled exceptions.
3. Handling Exceptions in ViewModelScope
In Android development, viewModelScope
is used to launch coroutines in ViewModels. However, uncaught exceptions in viewModelScope
crash the app unless properly handled.
Example: Crashing ViewModel Without Handling
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
throw IOException("Network failure")
}
}
}
📌 Problem: The exception is uncaught and crashes the app.
Solution: Use CoroutineExceptionHandler
class MyViewModel : ViewModel() {
private val handler = CoroutineExceptionHandler { _, throwable ->
Log.e("Coroutine", "Caught: $throwable")
}
fun fetchData() {
viewModelScope.launch(handler) {
throw IOException("Network failure")
}
}
}
📌 Key Takeaway: Always attach a CoroutineExceptionHandler
to prevent crashes.
4. Exception Handling in Parallel Coroutines
When executing multiple tasks in parallel, one coroutine failing cancels the others.
Example: One Failing Coroutine Cancels the Other
val result1 = async { fetchUserData() }
val result2 = async { fetchPosts() }
val userData = result1.await() // If this fails, result2 is also canceled
val posts = result2.await()
📌 Problem: If fetchUserData()
fails, fetchPosts()
is also canceled.
Solution: Use supervisorScope
to Make Coroutines Independent
supervisorScope {
val userData = async { fetchUserData() }
val posts = async { fetchPosts() }
try {
userData.await()
posts.await()
} catch (e: Exception) {
Log.e("Coroutine", "One coroutine failed, but the other continued")
}
}
📌 Key Takeaway: supervisorScope
ensures one failure does not cancel everything.
5. Exception Handling in Flow (Cold Streams)
Flows stop execution if an exception occurs inside collect()
.
Example: Flow Crashes on Exception
flow {
emit(1)
throw IllegalStateException("Error in flow")
}.collect {
println(it) // This stops execution after first emit
}
Solution: Use catch {}
to Handle Flow Exceptions
flow {
emit(1)
throw IllegalStateException("Error in flow")
}
.catch { e -> Log.e("Flow", "Caught exception: $e") }
.collect { println(it) }
📌 Key Takeaway: Always use .catch {}
to handle errors inside a Flow.
6. Retrying Failed Coroutines with Exponential Backoff
If a coroutine fails due to a network error, we can retry with exponential backoff.
suspend fun fetchDataWithRetry(): String {
var attempt = 0
val maxAttempts = 3
while (attempt < maxAttempts) {
try {
return fetchUserData()
} catch (e: IOException) {
attempt++
delay(1000L * attempt) // Exponential backoff
}
}
throw IOException("Failed after 3 attempts")
}
📌 Key Takeaway: Implement retries with increasing delay to handle transient failures.
7. Exception Handling in WorkManager with CoroutineWorker
When using WorkManager with coroutines, exceptions inside workers do not automatically retry.
Example: A Worker That Fails Silently
class MyWorker(ctx: Context, params: WorkerParameters) :
CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
fetchData() // May fail
return Result.success()
}
}
📌 Problem: If fetchData()
fails, WorkManager does not retry.
Solution: Return Result.retry()
on Exception
override suspend fun doWork(): Result {
return try {
fetchData()
Result.success()
} catch (e: Exception) {
Result.retry() // Automatically retries on failure
}
}
📌 Key Takeaway: Use Result.retry()
to ensure automatic retries.
Conclusion
Mastering exception handling in coroutines ensures that your app is resilient, fault-tolerant, and reliable.
What tricky coroutine failures have you encountered? Let me know in the comments! 🚀

Dobri Kostadinov
Android Consultant | Trainer
Email me | Follow me on LinkedIn | Follow me on Medium | Buy me a coffee