Tutorials

    Kotlin Coroutines: Handling Parallel API Failures

    A
    AzeemFeb 27, 2026 · 5 min read
    159
    Kotlin Coroutines: Handling Parallel API Failures

    Kotlin Coroutines: Handling Parallel API Failures

    The Real-World Requirement

    “If one of the parallel API calls fails, I do NOT want the other calls to stop executing.”

    This is a very common production requirement — especially for dashboards, home screens, or independent data sources.

    To solve this properly, you must first understand how async, await, and structured concurrency actually work.

    Understanding the Foundation: async vs await

    Most confusion starts here.

    async and await do completely different things.

    • async → starts a coroutine immediately (eager by default)

    • await → waits for the result of that coroutine

    Think of it like this:

    • async = start cooking

    • await = collect the cooked food

    Cooking happens independently of when you collect it.

    Senior-level mental model:

    async defines concurrency.
    await defines synchronization.

    The Default Behavior: Why Everything Stops on Exception

    Consider this code:

    suspend fun parallelApiCallsCancellation() = coroutineScope {
    
        val a = async {
            delay(100)
            println("A done")
            1
        }
    
        val b = async {
            delay(1000)
            throw RuntimeException("API failed")
        }
    
        val c = async {
            delay(2000)
            println("C done")
            3
        }
    
        println(a.await())
    }
    
    kotlin

    Step-by-Step Execution Timeline

    ⏱ t = 0 ms

    coroutineScope starts.

    All three async blocks start immediately and concurrently:

    A → running (100ms)
    B → running (1000ms)
    C → running (2000ms)
    
    code

    You do NOT need to call await() for them to start.

    ⏱ t = 100 ms

    A completes:

    A done
    
    code

    It now holds result 1.

    Your main coroutine reaches:

    println(a.await())
    
    kotlin

    Since A is already completed:

    • await() does not suspend

    • It returns immediately

    Output becomes:

    A done
    1
    code

    ⏱ t = 1000 ms

    B throws:

    RuntimeException("API failed")
    
    kotlin

    Now structured concurrency rule applies:

    Child fails → Exception escapes → Parent cancelled → All siblings cancelled
    
    code

    So:

    • Parent coroutineScope is cancelled

    • C is cancelled immediately

    • Whole function fails

    You never see:

    C done
    code
    // A done and 1 print immediately. so question is is async await runs same time?
    code

    Why Does a.await() Print Immediately?

    Because async started at time 0.

    By the time you call a.await(), A has already completed in the background.

    So await() simply retrieves the already-computed result.

    If you had written:

    val a = async(start = CoroutineStart.LAZY) { ... }
    kotlin

    Then A would not start until await() is called.

    That is lazy execution.

    Job-Ready Android Bootcamp

    Master Android Development from fundamentals to advanced architecture. Understand every concept in depth, build real-world projects, and prepare yourself to crack high-paying Android developer roles.

    Enroll Now (Limited seats to maintain quality)

    So Let’s see The Core Problem Why coroutineScope failed ?

    coroutineScope is fail-fast.

    If one child fails:

    • Parent is cancelled

    • All siblings are cancelled

    • Entire operation fails

    But our requirement says:

    Other API calls must continue even if one fails.

    There are two correct solutions.

    Solution 1 — Using supervisorScope (Recommended)

    Replace coroutineScope with supervisorScope.

    suspend fun parallelApiCallsWithSupervisor() = supervisorScope {
    
        val a = async {
            delay(100)
            println("A done")
            1
        }
    
        val b = async {
            delay(1000)
            throw RuntimeException("API failed")
        }
    
        val c = async {
            delay(2000)
            println("C done")
            3
        }
    
        try {
            println(a.await())
            println(b.await())
            println(c.await())
        } catch (e: Exception) {
            println("Caught: ${e.message}")
        }
    }
    
    kotlin

    What Changed?

    supervisorScope uses a SupervisorJob.

    Rule comparison:

    coroutineScope:
    Child fails → Parent cancelled → Siblings cancelled
    
    supervisorScope:
    Child fails → Parent stays active → Siblings continue
    
    code

    Execution Flow

    • A completes → prints A done and 1

    • B fails → parent is NOT cancelled

    • b.await() throws → you catch it

    • C continues and completes → prints C done

    Final output:

    A done
    1
    Caught: API failed
    C done
    3
    
    code

    Solution 2 — Without Using supervisorScope

    Now the important interview question:

    How do you achieve the same behavior without supervisorScope?

    The Key Principle

    In coroutineScope, cancellation happens only when:

    An exception escapes the child coroutine.

    If you prevent the exception from escaping, the parent never sees failure.

    So the solution is:

    Catch the exception inside async.

    Correct Approach — try-catch Inside async

    suspend fun parallelApiCallsWithoutSupervisor() = coroutineScope {
    
        val a = async {
            delay(100)
            println("A done")
            1
        }
    
        val b = async {
            try {
                delay(1000)
                throw RuntimeException("API failed")
            } catch (e: Exception) {
                println("B failed safely")
                null
            }
        }
    
        val c = async {
            delay(2000)
            println("C done")
            3
        }
    
        println(a.await())
        println(b.await())
        println(c.await())
    }
    
    kotlin

    Why This Works

    When B throws:

    • Exception is caught inside the async

    • It never escapes

    • Parent is never cancelled

    • C continues running

    Final output:

    A done
    1
    B failed safely
    null
    C done
    3
    code

    The Common Mistake: Catching at await()

    Many developers write this:

    try {
        b.await()
    } catch (e: Exception) {
        println("Caught")
    }
    
    kotlin

    This does NOT solve the problem in coroutineScope.

    Here is why:

    1. B throws

    2. Exception escapes async

    3. Parent is cancelled immediately

    4. C is cancelled

    5. Then await() throws

    6. You catch it — but it is too late

    C is already cancelled.

    So:

    • Catching at await() handles the crash

    • But does NOT protect siblings

    Production-Ready Clean Pattern

    Instead of manual try-catch, use runCatching.

    val a = async { runCatching { apiCallA() }.getOrNull() }
    val b = async { runCatching { apiCallB() }.getOrNull() }
    val c = async { runCatching { apiCallC() }.getOrNull() }
    
    val resultA = a.await()
    val resultB = b.await()
    val resultC = c.await()
    kotlin

    This:

    • Prevents exception from escaping

    • Keeps parent alive

    • Allows partial results

    • Avoids boilerplate

    Full Comparison

    Approach

    Parent Cancelled?

    Siblings Cancelled?

    Use Case

    coroutineScope (default)

    Yes

    Yes

    Fail-fast transactions

    supervisorScope

    No

    No

    Independent parallel APIs

    try-catch inside async

    No

    No

    When supervisorScope not used

    try-catch at await()

    Yes

    Yes

    Only prevents crash

    Conclusion

    If your requirement is:

    “One parallel API failure should not stop others.”

    You have two correct approaches:

    ✅ Preferred

    Use supervisorScope.

    ✅ Without supervisorScope

    Catch exceptions inside each async block so they never escape.

    The Core Principle

    Cancellation in structured concurrency happens only when an exception escapes the child coroutine.

    Control the exception propagation — and you control the cancellation behavior.

    That is the real understanding behind Kotlin Coroutines failure handling.

    If you like, hit the like button and join newsletter.

    android
    kotlin
    kotlin coroutines
    async await
    structured concurrency
    supervisorScope
    coroutineScope
    android architecture
    concurrency
    exception handling
    Was this helpful?

    Comments (0)

    No comments yet. Be the first to share your thoughts!

    Leave a Comment

    Get the latest updates

    Join our newsletter and never miss an article.

    By subscribing, you agree to our Privacy Policy and consent to receive updates.

    Ready to Start Learning?

    Join thousands of students building their dream careers

    Explore Courses