Kotlin Coroutines provides a structured concurrency model to handle async tasks efficiently. Among its key components, supervisorScope is essential when dealing with child coroutines that should not affect their siblings in case of failures.
In this blog, we will understand supervisorScope with detailed examples, internal workings, edge cases, best practices, and a real-world analogy inspired by Rajasthan's traditional systems.
Join our upcoming classes
Our Courses
Real-World Analogy: Rajasthan's Water Management System
Imagine a stepwell (baori) in Rajasthan, where multiple water storage sections exist. If one section collapses, the others continue functioning without affecting the whole system.
Similarly, supervisorScope ensures that if one coroutine fails, the others continue executing independently.
Another analogy is Rajasthan's district-level irrigation management—each district has its own water reserves, and failure in one area doesn’t lead to complete system collapse, unlike a centralized supply.
Understanding Coroutine Scope vs. Supervisor Scope
Before diving into supervisorScope, let's first understand the difference between CoroutineScope and SupervisorScope.
CoroutineScope
In a normal coroutine scope, if one child coroutine fails, it cancels the entire scope, affecting all other coroutines inside it.
import kotlinx.coroutines.*
fun main() {
runBlocking {
checkScope()
}
}
suspend fun checkScope() = coroutineScope {
launch {
delay(1000)
println("Task 1 Completed")
}
launch {
delay(500)
throw RuntimeException("Task 2 Failed")
}
}Output:
Exception in thread "main" java.lang.RuntimeException: Task 2 FailedHere, Task 1 is also canceled because Task 2 failed, demonstrating that all children are affected.
Introducing supervisorScope
With supervisorScope, a failing child does not affect its siblings. This ensures independent execution of tasks within the same parent scope.
import kotlinx.coroutines.*
fun main() {
runBlocking {
checkScope()
}
}
suspend fun checkScope() = supervisorScope {
launch {
delay(1000)
println("Task 1 Completed")
}
launch {
delay(500)
throw RuntimeException("Task 2 Failed")
}
}Output:
Exception in thread "main" java.lang.RuntimeException: Task 2 Failed
Task 1 CompletedNow, Task 1 successfully completes execution even though Task 2 fails.
Internal Working of supervisorScope
Internally, supervisorScope creates a SupervisorJob, which ensures that the failure of one child coroutine does not cancel the entire scope.
suspend fun supervisorScope(block: suspend CoroutineScope.() -> Unit) {
coroutineScope {
val supervisorJob = SupervisorJob()
val newScope = this + supervisorJob
newScope.block()
}
}SupervisorJoballows child coroutines to work independently.Unlike
CoroutineScope, it does not propagate failure to siblings.It still ensures structured concurrency within the block.
Best Practices and Edge Cases
When supervisorScope Might Not Be Ideal
If you need complete failure rollback,
supervisorScopeis not suitable.If the failure of one task should halt everything (e.g., a transaction system), use a regular coroutine scope.
Common Mistakes to Avoid
Forgetting to handle failures:
supervisorScopedoesn’t prevent failures; you must handle them inside coroutines usingtry-catch.Using
supervisorScopeat the top level: It’s better suited for handling specific independent child tasks rather than being applied to an entire application scope.
Real-World Example: Managing Rajasthan Tourism Bookings
Imagine an online tourism system for Rajasthan:
A user books a Desert Safari and a City Palace Tour together.
If the Desert Safari fails due to weather conditions, the City Palace Tour should still proceed.
Using supervisorScope, we can model this system:
import kotlinx.coroutines.*
fun main() {
runBlocking {
rajasthanTrip()
}
}
suspend fun rajasthanTrip() = supervisorScope {
launch {
try {
delay(500)
throw Exception("Desert Safari Booking Failed")
} catch (e: Exception) {
println(e.message)
}
}
launch {
delay(1000)
println("City Palace Tour Confirmed")
}
}Output:
Desert Safari Booking Failed
City Palace Tour ConfirmedEven though the safari booking fails, the palace tour continues as planned.
When to Use supervisorScope
Use supervisorScope when:
You have multiple independent coroutines where one failure should not affect others.
You are handling UI operations where a failed network call should not crash the entire screen.
You need isolated error handling without canceling the entire scope.
Exception Handling in supervisorScope
While supervisorScope prevents failures from propagating to sibling coroutines, it does not automatically handle exceptions. You still need to catch them properly within each coroutine.
Handling Exceptions with try-catch
import kotlinx.coroutines.*
fun main() {
runBlocking {
rajasthanTrip()
}
}
suspend fun rajasthanTrip() = supervisorScope {
launch {
try {
delay(500)
throw Exception("Task 1 Failed")
} catch (e: Exception) {
println("Caught Exception in Task 1: ${e.message}")
}
}
launch {
delay(1000)
println("Task 2 Completed Successfully")
}
}Output:
Caught Exception in Task 1: Task 1 Failed
Task 2 Completed SuccessfullyHere, Task 2 continues running because the failure in Task 1 is handled gracefully.
Handling Exceptions with CoroutineExceptionHandler
Instead of using try-catch inside each coroutine, you can use a CoroutineExceptionHandler to catch all uncaught exceptions.
import kotlinx.coroutines.*
fun main() {
runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught Exception: ${exception.message}")
}
rajasthanTrip(exceptionHandler)
}
}
suspend fun rajasthanTrip(exceptionHandler: CoroutineExceptionHandler) = supervisorScope {
launch(exceptionHandler) {
delay(500)
throw Exception("Desert Safari Booking Failed")
}
launch {
delay(1000)
println("City Palace Tour Confirmed")
}
}Output:
Caught Exception: Desert Safari Booking Failed
City Palace Tour ConfirmedCombining supervisorScope with Retry Mechanism
For transient failures (e.g., network issues), you can retry the failing coroutine.
import kotlinx.coroutines.*
import kotlin.random.Random
fun main() {
runBlocking {
rajasthanTrip()
}
}
suspend fun retryTask(): String {
repeat(3) { attempt ->
try {
delay(500)
if (Random.nextBoolean()) throw Exception("Random Failure")
return "Success on attempt ${attempt + 1}"
} catch (e: Exception) {
println("Attempt ${attempt + 1} failed: ${e.message}")
}
}
return "Failed after retries"
}
suspend fun rajasthanTrip() = supervisorScope {
launch {
val result = retryTask()
println(result)
}
launch {
delay(1000)
println("Other task running normally")
}
}Output:
Attempt 1 failed: Random Failure
Attempt 2 failed: Random Failure
Other task running normally
Attempt 3 failed: Random Failure
Failed after retriesConclusion
supervisorScope is a powerful feature in Kotlin Coroutines that ensures failure resilience. By using it, we can create robust applications where independent tasks execute without unnecessary cancellations.
Just like Rajasthan's ancient water systems ensured survival despite failures in certain sections, supervisorScope keeps coroutines running even when some fail.
Key Takeaways:
supervisorScopeprevents failures from propagating to sibling coroutines.It internally uses
SupervisorJobfor structured concurrency.Best suited for independent tasks where failures should be isolated.
Always handle exceptions properly within
supervisorScope.
Akshay Nandwana
Founder AndroidEngineers
You can connect with me on:
Book 1:1 Session here Click Here
Join our upcoming classes
Our Courses
Get the latest Android development articles delivered to your inbox.