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
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.
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")
}
}
Exception in thread "main" java.lang.RuntimeException: Task 2 Failed
Here, Task 1
is also canceled because Task 2
failed, demonstrating that all children are affected.
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")
}
}
Exception in thread "main" java.lang.RuntimeException: Task 2 Failed
Task 1 Completed
Now, Task 1
successfully completes execution even though Task 2
fails.
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()
}
}
SupervisorJob
allows child coroutines to work independently.
Unlike CoroutineScope
, it does not propagate failure to siblings.
It still ensures structured concurrency within the block.
supervisorScope
Might Not Be IdealIf you need complete failure rollback, supervisorScope
is not suitable.
If the failure of one task should halt everything (e.g., a transaction system), use a regular coroutine scope.
Forgetting to handle failures: supervisorScope
doesn’t prevent failures; you must handle them inside coroutines using try-catch
.
Using supervisorScope
at the top level: It’s better suited for handling specific independent child tasks rather than being applied to an entire application scope.
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")
}
}
Desert Safari Booking Failed
City Palace Tour Confirmed
Even though the safari booking fails, the palace tour continues as planned.
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.
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.
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")
}
}
Caught Exception in Task 1: Task 1 Failed
Task 2 Completed Successfully
Here, Task 2
continues running because the failure in Task 1
is handled gracefully.
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")
}
}
Caught Exception: Desert Safari Booking Failed
City Palace Tour Confirmed
supervisorScope
with Retry MechanismFor 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")
}
}
Attempt 1 failed: Random Failure
Attempt 2 failed: Random Failure
Other task running normally
Attempt 3 failed: Random Failure
Failed after retries
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.
supervisorScope
prevents failures from propagating to sibling coroutines.
It internally uses SupervisorJob
for 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:
Join our upcoming classes
Our Courses