Coroutines in Kotlin are structured. This means when you launch a coroutine inside another coroutine,
they form a parentβchild relationship. Each coroutine is backed by a Job object,
and together, they create a job hierarchy.
Every coroutine has a Job that represents its lifecycle.
A Job can be in one of several states: Active, Completing, Cancelling, or Completed.
When you cancel a parent job, all its children are automatically cancelled.
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob = launch {
val childJob1 = launch {
repeat(3) { i ->
println("Child 1 - Working $i")
delay(200)
}
println("Child 1 - Done")
}
val childJob2 = launch {
repeat(3) { i ->
println("Child 2 - Working $i")
delay(300)
}
println("Child 2 - Done")
}
// Wait for children to finish
childJob1.join()
childJob2.join()
println("Parent job completed")
}
delay(500)
println("Cancelling parent job!")
parentJob.cancelAndJoin()
println("Parent cancelled β all children stopped.")
}
parentJob is launched using launch inside runBlocking.parentJob.cancel() is called, both childJob1 and childJob2 are automatically cancelled.Child 1 - Working 0 Child 2 - Working 0 Child 1 - Working 1 Child 2 - Working 1 Cancelling parent job! Parent cancelled β all children stopped.
Notice: The children stop mid-way β they never print βDoneβ, because the parent cancelled them.
A child coroutine automatically inherits its parentβs CoroutineContext β
this includes the dispatcher, job, and exception handling behavior.
But you can override any part of it (for example, by changing the dispatcher or adding a new exception handler).
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, ex ->
println("Caught exception: ${'$'}{ex.message}")
}
val parentJob = launch(handler) {
val childJob = launch {
println("Child job starting")
throw RuntimeException("Child failed!")
}
childJob.join()
println("Parent continues even after child failure?")
}
parentJob.join()
println("Done.")
}
launch inside launch,
the exception propagates up and cancels the parent.CoroutineExceptionHandler catches it at the top level.Child job starting Caught exception: Child failed! Done.
The parent was cancelled because its child failed β unless we use a SupervisorJob (explained below).
By default, when one child coroutine fails, the failure cancels the parent and all other children. This is sometimes undesirable β for example, when multiple independent tasks run in parallel.
To prevent cascading failures, Kotlin provides SupervisorJob or supervisorScope.
import kotlinx.coroutines.*
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val child1 = scope.launch {
println("Child 1 starting")
throw RuntimeException("Child 1 failed!")
}
val child2 = scope.launch {
delay(500)
println("Child 2 completed successfully")
}
joinAll(child1, child2)
println("Supervisor scope is still alive even after failure")
}
SupervisorJob creates a parent job where children are independent.child1 fails, the exception does not cancel the supervisor or child2.child2 continues and finishes normally.Child 1 starting Child 2 completed successfully Supervisor scope is still alive even after failure
supervisorScope { ... } is a suspending function that behaves like coroutineScope,
but it uses a SupervisorJob internally.
import kotlinx.coroutines.*
fun main() = runBlocking {
supervisorScope {
val child1 = launch {
println("Child 1 started")
throw RuntimeException("Child 1 crashed")
}
val child2 = launch {
delay(400)
println("Child 2 done")
}
child1.join()
child2.join()
}
println("Supervisor scope completed")
}
supervisorScope prevents one childβs failure from cancelling others.Child 1 started Child 2 done Supervisor scope completed
runBlocking (root job)
β
βββ parentJob
βββ childJob1
βββ childJob2
parentJob is cancelled β all children are cancelled.childJob1 fails β parent and childJob2 are also cancelled (default).SupervisorJob β only the failing child is cancelled, others continue.| Feature | Job | SupervisorJob |
|---|---|---|
| Failure of one child affects siblings? | β Yes, cancels all | β No, isolated |
| Parent cancelled β all children? | β Yes | β Yes |
| Used with | launch, coroutineScope |
launch, supervisorScope |
In short: Use SupervisorJob when you want sibling coroutines to work independently,
even if one of them fails.