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 cookingawait= collect the cooked food
Cooking happens independently of when you collect it.
Senior-level mental model:
asyncdefines concurrency.awaitdefines 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())
}
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)
You do NOT need to call await() for them to start.
⏱ t = 100 ms
A completes:
A done
It now holds result 1.
Your main coroutine reaches:
println(a.await())
Since A is already completed:
await()does not suspendIt returns immediately
Output becomes:
A done
1⏱ t = 1000 ms
B throws:
RuntimeException("API failed")
Now structured concurrency rule applies:
Child fails → Exception escapes → Parent cancelled → All siblings cancelled
So:
Parent
coroutineScopeis cancelledC is cancelled immediately
Whole function fails
You never see:
C done// A done and 1 print immediately. so question is is async await runs same time?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) { ... }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}")
}
}
What Changed?
supervisorScope uses a SupervisorJob.
Rule comparison:
coroutineScope:
Child fails → Parent cancelled → Siblings cancelled
supervisorScope:
Child fails → Parent stays active → Siblings continue
Execution Flow
A completes → prints
A doneand1B fails → parent is NOT cancelled
b.await()throws → you catch itC continues and completes → prints
C done
Final output:
A done
1
Caught: API failed
C done
3
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())
}
Why This Works
When B throws:
Exception is caught inside the
asyncIt never escapes
Parent is never cancelled
C continues running
Final output:
A done
1
B failed safely
null
C done
3The Common Mistake: Catching at await()
Many developers write this:
try {
b.await()
} catch (e: Exception) {
println("Caught")
}
This does NOT solve the problem in coroutineScope.
Here is why:
B throws
Exception escapes
asyncParent is cancelled immediately
C is cancelled
Then
await()throwsYou catch it — but it is too late
C is already cancelled.
So:
Catching at
await()handles the crashBut 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()This:
Prevents exception from escaping
Keeps parent alive
Allows partial results
Avoids boilerplate
Full Comparison
Approach | Parent Cancelled? | Siblings Cancelled? | Use Case |
|---|---|---|---|
| Yes | Yes | Fail-fast transactions |
| No | No | Independent parallel APIs |
try-catch inside | No | No | When supervisorScope not used |
try-catch at | 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.
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.