Blog Infos
Author
Published
Topics
, , , ,
Published
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 concurrencyexception 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 asynclaunch, and supervisorScope
✅ Managing errors in FlowSharedFlow, 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 until await() 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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

No results found.

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

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu