kotlinandroid-jetpack-composekotlin-coroutineskotlin-multiplatform

Parent coroutines does not cancels other child coroutines after an exception


If the scope does not have SupervisorJob() then the failure of one coroutine child will be resulting in the failure of the parent scope and all other children of the parent scope will fail.

If the above statement is correct then in the example code child 2 coroutine should not be printed as the first coroutine is failed with a thrown exception.

But I still get this output:

child 1 coroutine
child 2 coroutine

What's the difference when using a SupervisorJob with val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())?

val scope = CoroutineScope(Dispatchers.IO)

Button(onClick = {
    scope.launch {
        launch {
            println("child 1 coroutine")
            delay(2.seconds)
            throw CancellationException()
        }
        delay(4.seconds)
        launch {
            println("child 2 coroutine")
        }
    }
}) {
    Text(text = "Scope check")
}

ANS : For best explanation read below Ans which is marked as correct from @Leviathan

Crt code to handle exception

val handler = CoroutineExceptionHandler { _, e ->
                    println("handled: ${e.message}")
                }
                val scope = CoroutineScope(Dispatchers.IO + handler)
                Button(
                    modifier = Modifier.padding(innerPadding),
                    onClick = {
                        scope.launch {
                            supervisorScope {
                                launch {
                                    println("child 1 coroutine")
                                    delay(1.seconds)
                                    throw Exception("Child 1 Exception")
                                }
                                delay(4.seconds)
                                launch {
                                    println("child 2 coroutine")
                                }
                            }

                        }
                    }) {
                    Text(text = "Scope check")
                }

Solution

  • There are several issues with your example code. Before I'll try to explain in more detail let's first clarify how to call the various coroutines that are involved:

    The following misconceptions prevent your code from behaving as expected:

    1. Throwing a CancellationException just cancels the current job so it stops. Cancellation only propagates down the job hierarchy, not up. That's why the parent job doesn't see a failed coroutine. Therefore the second coroutine is not affected and completes successfully.

      So, what you actually want in your example code is to fail the coroutine. For that you need to replace the CancellationException with any other kind of exception, like Exception().

    2. The second coroutine is only launched after the delay(4.seconds): When the first coroutine fails after two seconds, the second coroutine isn't even launched yet. This can be easily fixed by moving the 4 second delay inside the launch block of the second coroutine. Your example code should look like this now:

      scope.launch {
          launch {
              println("child 1 coroutine")
              delay(2.seconds)
              throw Exception("child 1 failed")
          }
      
          launch {
              delay(4.seconds)
              println("child 2 coroutine")
          }
      }
      

      What happens now is that both coroutines are immediately launched. After two seconds the first coroutine fails. Since you haven't (yet) specified a SupervisorJob a failed child coroutine leads to a failure of the parent coroutine. And that leads to a cancellation of all remaining child coroutines, in this case the second coroutine. That leads to the expected behavior: child 2 coroutine is never printed.

    3. When you then change the scope to use a SupervisorJob you may think that child 2 coroutine will now be printed. That is not the case, though: A SupervisorJob only protects its direct children in case a child fails. Since you provided the SupervisorJob to the scope only coroutines launched in that scope are protected. The first and the second child coroutines, however, are launched in the scope of the parent coroutine so they have that job as their parent, not the SupervisorJob. You can remedy that with any of the following options:

      • Also launch the two child coroutines with scope.launch.
      • Instead wrap the two child coroutines in supervisorScope { /*...*/ }.
      • Instead assign the SupervisorJob to a variable (val supervisor = SupervisorJob()) and provide it as the context for the two child coroutines: launch(supervisor) { /*...*/ }
    4. Although the failure of the first coroutine doesn't fail the parent coroutine anymore so the second coroutine keeps running, the actual exception never got handled. And because this unhandled exception bubbles up the job hierarchy and never finds a handler, the top-most job fails and all coroutines in the hierarchy are cancelled, including the supervisor job and therefore also the second coroutine. And then the unhandled exception is rethrown and your app crashes.

      What you are missing is a CoroutineExceptionHandler as explained in the SupervisorJob documentation:

      A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, so a supervisor can implement a custom policy for handling failures of its children:

      • A failure of a child job that was created using launch can be handled via CoroutineExceptionHandler in the context.

      [...]

      Let's assume you use this handler:

      val handler = CoroutineExceptionHandler { _, e ->
          println("handled: ${e.message}")
      }
      

      Then you can provide the handler as context for the entire scope:

      val scope = CoroutineScope(Dispatchers.IO + handler)
      

      Or you can specify it only for the parent coroutine:

      scope.launch(handler) { /*...*/ }
      

      In both cases the exception will be handled so it isn't bubbled up the job hierarchy and won't eventually fail all jobs.

    And only now you will get the expected output:

    child 1 coroutine
    handled: child 1 failed
    child 2 coroutine