Coroutines in Kotlin are designed to support structured concurrency, ensuring that tasks launched within a scope adhere to a predictable lifecycle. When working with coroutines, it is essential to understand the relationship between parent and child coroutines, especially in scenarios involving exceptions, cancellations, and scope management. In this article, we'll explore these relationships in detail with examples.
Parent and Child Coroutines
In Kotlin, coroutines launched using builders like launch
or async
within a coroutine scope automatically form a parent-child relationship. This relationship governs how exceptions and cancellations propagate between coroutines.
Key Characteristics of Parent-Child Coroutines
Cancellation Propagation:
If the parent coroutine is cancelled, all its child coroutines are also cancelled.
If a child coroutine fails (throws an exception), the parent coroutine is cancelled by default unless a special construct like
SupervisorJob
is used.
Structured Concurrency:
Parent coroutines do not complete until all their child coroutines have completed. This ensures a predictable execution flow.
Error Propagation:
Exceptions thrown in a child coroutine propagate to the parent, which can handle the exception or let it crash the application.
Example: Parent Cancels Child
Here's an example demonstrating how cancellation propagates from parent to child:
import kotlinx.coroutines.* fun main() = runBlocking { val parentJob = launch { val childJob = launch { repeat(10) { i -> println("Child is working: $i") delay(500) } } delay(1200) // Allow child to run for a while println("Parent is cancelling") childJob.cancelAndJoin() // Cancels the child job } parentJob.join() println("Parent completed") }
Output:
Child is working: 0
Child is working: 1
Parent is cancelling
Parent completed
In this example, the parent coroutine explicitly cancels its child, ensuring proper resource cleanup.
Exceptions in Child Coroutines
When a child coroutine throws an unhandled exception, it propagates to the parent. By default, this cancels the parent and any sibling coroutines.
Example: Child Throws Exception
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob = launch {
launch {
throw IllegalArgumentException("Child coroutine failed!")
}
launch {
repeat(5) {
println("Sibling is working")
delay(300)
}
}
}
parentJob.join()
println("Parent completed")
}
Output:
Exception in thread "main" java.lang.IllegalArgumentException: Child coroutine failed!
The exception in one child causes the parent to cancel, which in turn cancels its sibling.
Handling Exceptions with SupervisorJob
Using a SupervisorJob
allows exceptions in a child coroutine to not affect siblings or the parent coroutine.
Example: Isolating Failures with SupervisorScope
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob = supervisorScope {
launch {
throw IllegalArgumentException("Child coroutine failed!")
}
launch {
repeat(5) {
println("Sibling is working")
delay(300)
}
}
}
println("Parent completed")
}
Output:
Sibling is working
Sibling is working
Sibling is working
Sibling is working
Sibling is working
Parent completed
In this example, the failure of one child does not affect its sibling or the parent.
CoroutineExceptionHandler
A CoroutineExceptionHandler
provides a centralized way to handle uncaught exceptions in a coroutine scope.
Example: Using CoroutineExceptionHandler
import kotlinx.coroutines.*
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: ${exception.message}")
}
val parentJob = launch(exceptionHandler) {
launch {
throw IllegalArgumentException("Child coroutine failed!")
}
}
parentJob.join()
println("Parent completed")
}
Output:
Caught exception: Child coroutine failed!
Parent completed
The CoroutineExceptionHandler
prevents the application from crashing and gracefully logs the exception.
Summary
Parent and child coroutines form a structured hierarchy where cancellation and exceptions propagate by default.
The use of
SupervisorJob
orSupervisorScope
isolates failures, ensuring one child’s failure does not cancel its siblings.A
CoroutineExceptionHandler
allows centralized exception handling to gracefully manage errors.
By understanding these concepts, you can design robust, maintainable, and predictable coroutine-based applications in Kotlin.
📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! 👇
Happy coding! 💻✨