Job Hierarchy in Kotlin Coroutines

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.

1️⃣ What is a Job?

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.")
}
  

🧠 Explanation

πŸ’» Expected Output

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.


2️⃣ Propagation of Context and Exceptions

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.")
}
  

πŸ’¬ Explanation

πŸ’» Output

Child job starting
Caught exception: Child failed!
Done.
  

The parent was cancelled because its child failed β€” unless we use a SupervisorJob (explained below).


3️⃣ SupervisorJob β€” Isolating Failures

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.

βœ… Example β€” Using SupervisorJob


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")
}
  

πŸ’¬ Explanation

πŸ’» Output

Child 1 starting
Child 2 completed successfully
Supervisor scope is still alive even after failure
  

4️⃣ supervisorScope β€” Structured Alternative

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")
}
  

πŸ’¬ Explanation

πŸ’» Output

Child 1 started
Child 2 done
Supervisor scope completed
  

5️⃣ Visual Summary β€” Job Hierarchy Tree (Text Representation)

runBlocking (root job)
β”‚
└── parentJob
    β”œβ”€β”€ childJob1
    └── childJob2
  

🧾 Quick Comparison

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.