Supervisor-Job

SupervisorJob is similar to a regular Job with the difference that cancellation is propagated only downwards, which means:

  • Children can fail independently of each other.
  • Does not auto-fail parent.

A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children.

Straightforward Examples

SupervisorJob() added at scope level allows one child to finish, when the other child threw exception

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess

fun main(): kotlin.Unit = runBlocking {
  val out = Out.standard()

  out.info("START")

  try {
    val coroutineScope = CoroutineScope(
      Dispatchers.Default + SupervisorJob()
    )

    val coRoutine1Deferred = coroutineScope.async(CoroutineName("WillFail")) {
      val timeMillis = 2000L
      out.info("This one will throw in $timeMillis ms")
      delay(timeMillis)
      val exc = MyExceptionWillThrowFromCoroutine("I-Failed-In-CoRoutine")
      out.warn("${Emojis.EXCEPTOIN} I am throwing [${exc::class.simpleName}/${exc.message}]! ${Emojis.EXCEPTOIN}")
      throw exc

      "ResultFromWillFail"
    }

    val coRoutine2Deferred = coroutineScope.async(CoroutineName("JustPrints")) {
      (0..10)
        .map { "a-${it}" }
        .forEach {
          out.info(it)

          try {
            delay(500)
          } catch (e: CancellationException) {
            val excMsg = e.message ?: e.toString()
            out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ")

            throw e
          }
        }
    }

    // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws
    // MyExceptionWillThrowFromCoroutine
    out.info("co-routine-2 result: " + coRoutine2Deferred.await())

    out.info("Successfully awaited co-routine-2, now awaiting co-routine-1")

    out.info("co-routine-1 result: " + coRoutine1Deferred.await())
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

  out.info("DONE - without errors on main")
}

class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)

Command to reproduce:

gt.sandbox.checkout.commit fc38283452b69eaf78fc \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   14ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   33ms][2️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-1/tid:31] This one will throw in 2000 ms
[INFO][elapsed:   37ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-0
[INFO][elapsed:  538ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-1
[INFO][elapsed: 1039ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-2
[INFO][elapsed: 1539ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-3
[INFO][elapsed: 2040ms][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] a-4
[WARN][elapsed: 2068ms][3️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-2/tid:32] 💥  I am throwing [MyExceptionWillThrowFromCoroutine/I-Failed-In-CoRoutine]! 💥 
[INFO][elapsed: 2541ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-5
[INFO][elapsed: 3041ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-6
[INFO][elapsed: 3542ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-7
[INFO][elapsed: 4043ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-8
[INFO][elapsed: 4543ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-9
[INFO][elapsed: 5044ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-10
[INFO][elapsed: 5546ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-2 result: kotlin.Unit
[INFO][elapsed: 5546ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Successfully awaited co-routine-2, now awaiting co-routine-1
[ERROR][elapsed: 5560ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[MyExceptionWillThrowFromCoroutine] with msg=[I-Failed-In-CoRoutine] cause=[com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: I-Failed-In-CoRoutine]. Exiting with error code 1

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:run'.
> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 5s
SupervisorJob() added at 'async' level of throwing sibling allows the other sibling to finish

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess

fun main(): kotlin.Unit = runBlocking {
  val out = Out.standard()

  out.info("START")

  try {
    val coroutineScope = CoroutineScope(
      Dispatchers.Default
    )

    val coRoutine1Deferred = coroutineScope.async(
      CoroutineName("WillFail") + SupervisorJob()
    ) {
      val timeMillis = 2000L
      out.info("This one will throw in $timeMillis ms")
      delay(timeMillis)
      val exc = MyExceptionWillThrowFromCoroutine("I-Failed-In-CoRoutine")
      out.warn("${Emojis.EXCEPTOIN} I am throwing [${exc::class.simpleName}/${exc.message}]! ${Emojis.EXCEPTOIN}")
      throw exc

      "ResultFromWillFail"
    }

    val coRoutine2Deferred = coroutineScope.async(CoroutineName("JustPrints")) {
      (0..10)
        .map { "a-${it}" }
        .forEach {
          out.info(it)

          try {
            delay(500)
          } catch (e: CancellationException) {
            val excMsg = e.message ?: e.toString()
            out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ")

            throw e
          }
        }
    }

    // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws
    // MyExceptionWillThrowFromCoroutine
    out.info("co-routine-2 result: " + coRoutine2Deferred.await())

    out.info("Successfully awaited co-routine-2, now awaiting co-routine-1")

    out.info("co-routine-1 result: " + coRoutine1Deferred.await())
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

  out.info("DONE - without errors on main")
}

class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)

Command to reproduce:

gt.sandbox.checkout.commit 2156e50526a56711a2e8 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   36ms][2️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-1/tid:31] This one will throw in 2000 ms
[INFO][elapsed:   38ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-0
[INFO][elapsed:  540ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-1
[INFO][elapsed: 1041ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-2
[INFO][elapsed: 1542ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-3
[INFO][elapsed: 2042ms][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] a-4
[WARN][elapsed: 2071ms][3️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-2/tid:32] 💥  I am throwing [MyExceptionWillThrowFromCoroutine/I-Failed-In-CoRoutine]! 💥 
[INFO][elapsed: 2543ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-5
[INFO][elapsed: 3044ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-6
[INFO][elapsed: 3544ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-7
[INFO][elapsed: 4045ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-8
[INFO][elapsed: 4546ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-9
[INFO][elapsed: 5047ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-10
[INFO][elapsed: 5548ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-2 result: kotlin.Unit
[INFO][elapsed: 5549ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Successfully awaited co-routine-2, now awaiting co-routine-1
[ERROR][elapsed: 5563ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[MyExceptionWillThrowFromCoroutine] with msg=[I-Failed-In-CoRoutine] cause=[com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: I-Failed-In-CoRoutine]. Exiting with error code 1

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:run'.
> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 5s
We are able to cancel Deferred(Job) that used SupervisorJob() to be created by calling cancel() directly on deferred object

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess

fun main(): kotlin.Unit = runBlocking {
  val out = Out.standard()
  out.info("START")

  try {
    val scope = CoroutineScope(
      Dispatchers.Default
    )

    val deferred = scope.async(CoroutineName("JustPrints") + SupervisorJob()) {
      (0..10)
        .map { "a-${it}" }
        .forEach {
          out.info(it)

          try {
            delay(500)
          } catch (e: CancellationException) {
            val excMsg = e.message ?: e.toString()
            out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ")

            throw e
          }
        }
    }

    out.info("delay 1500 before cancelling co-routine 1")
    delay(1500)
    out.info("Cancelling co-routine 1 - ${Emojis.CANCELLATION}")
    deferred.cancel()
    out.info("delay before deferred.await()")
    delay(1500)
    out.info("calling deferred.await()")

    // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws
    // MyExceptionWillThrowFromCoroutine
    out.info("co-routine-1 result: " + deferred.await())
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

  out.info("DONE - without errors on main")
}

class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)

Command to reproduce:

gt.sandbox.checkout.commit c1640d7e2e9cfd437d54 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   34ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay 1500 before cancelling co-routine 1
[INFO][elapsed:   36ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-0
[INFO][elapsed:  538ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-1
[INFO][elapsed: 1038ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-2
[INFO][elapsed: 1536ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Cancelling co-routine 1 - ❌
[INFO][elapsed: 1538ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay before deferred.await()
[WARN][elapsed: 1583ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] 🫡 I have caught [JobCancellationException/DeferredCoroutine was cancelled], and rethrowing it 🫡 
[INFO][elapsed: 3039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] calling deferred.await()
[ERROR][elapsed: 3040ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[JobCancellationException] with msg=[DeferredCoroutine was cancelled] cause=[kotlinx.coroutines.JobCancellationException: DeferredCoroutine was cancelled; job="JustPrints#2":DeferredCoroutine{Cancelled}@3a03464]. Exiting with error code 1

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:run'.
> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 3s

Trickier examples

When a coroutine has its own (independent) job, it has nearly no relation to its parent. It only inherits other contexts, but other results of the parent-child relationship will not apply. This causes us to lose structured concurrency, which is a problematic situation that should be avoided. - Kotlin Coroutines Deep Dive

too many nested note refs

Side note

You typically do NOT need to create Job()/SupervisorJob() by hand. Look at Coroutine scope function and Error Boundary instead of creating Job()/SupervisorJob() by hand.


Children
  1. SupervisorJob() examples

Backlinks