Scope with Regular Job

What is a "Scope with Regular Job"?

A Scope with Regular Job in Kotlin coroutines refers to any CoroutineScope that uses Job() and hence abides by default exception handling behavior - where uncaught non-cancellation exceptions in child coroutine Cancels (shutsdown) its entire hierarchy of co-routines.

Default Exception Behavior

In ALL scopes that use Regular Job:

Highlight

Job - provide another avenue of stopping work other than traditional exceptions. Separate cancellation mechanism that is not as apparent as typical exception flow, where failed co-routine cancel's all co-routines that are related to it through Job hierarchy.

  • Each coroutine has its own Job instance.
  • Each Job instance is tied to it's parent job (if it has a parent Job). This tie in enables cancellation behavior.

Example

If regular Job() were used. If Coroutine#6 throws non-CancellationException exception the entire hierarchy is gonig to be cancelled and Coroutine#1 will rethrow the exception that Coroutine#6 threw.

img

Creating Error Boundary

Gotchas

Code example

too many nested note refs

Instead of manually setting up scope and waiting on it we can use coroutineScope

too many nested note refs

Next let's see how the cancellations are handled, we expect any single failure to cancel all others.

too many nested note refs

Now let's have one of co-routines cancel itself, which should not disturb other co-routines.

too many nested note refs

With await:

too many nested note refs

too many nested note refs

Cancellation of Parent Example

too many nested note refs

custom scope - with await - exception from the co-routine will be rethrown parent when we await, BUT siblings are cancelled RIGHT away when uncaught exception happens.

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.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.seconds

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

  try {
    val myScope = CoroutineScope(
      Dispatchers.Default
    )

    val coRoutine1Deferred = myScope.async(
      CoroutineName("WillFail")
    ) {
      val timeMillis = 1000L
      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 = myScope.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
          }
        }
    }

    out.actionWithMsg(
      actionThatWeAreDelayingFor = "coRoutine1Deferred.join()",
      action = {
        coRoutine1Deferred.join()
      }
    )
    out.infoPrintState(coRoutine1Deferred, "coRoutine1Deferred")
    out.info("myScope.isActive = ${myScope.isActive}")

    out.delayedActionWithMsg(
      actionThatWeAreDelayingFor = "coRoutine1Deferred.await()",
      delayBeforeAction = 2.seconds,
      action = {
        out.info("coRoutine1Deferred.await()=[${coRoutine1Deferred.await()}]")
      }
    )

    out.delayedActionWithMsg(
      actionThatWeAreDelayingFor = "coRoutine2Deferred.await()",
      delayBeforeAction = 2.seconds,
      action = {
        out.info("coRoutine1Deferred.await()=[${coRoutine2Deferred.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 17ff993a20b91dfff9ea \
&& 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 1000 ms
[INFO][elapsed:   35ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[coRoutine1Deferred.join()]
[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][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] a-2
[WARN][elapsed: 1068ms][3️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-2/tid:32] 💥  I am throwing [MyExceptionWillThrowFromCoroutine/I-Failed-In-CoRoutine]! 💥 
[INFO][elapsed: 1070ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[coRoutine1Deferred.join()]
[INFO][elapsed: 1073ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [coRoutine1Deferred]:
coRoutine1Deferred.isActive    | false
coRoutine1Deferred.isCancelled | true
coRoutine1Deferred.isCompleted | true

[INFO][elapsed: 1073ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] myScope.isActive = false
[INFO][elapsed: 1076ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] 2s delay, before_action=[coRoutine1Deferred.await()]
[WARN][elapsed: 1083ms][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡 
[INFO][elapsed: 3077ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Done with=2s delay performing action=[coRoutine1Deferred.await()]
[ERROR][elapsed: 3079ms][🥇][①][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 3s

Also see:

too many nested note refs

An optional element in the coroutine context to handle uncaught exceptions. - kdoc

Highlights

  • Handles uncaught exceptions.
  • Does not work with async.

CoroutineExceptionHandler does NOT work with async

too many nested note refs

CoroutineExceptionHandler works with launch

CoroutineExceptionHandler processes uncaught exception from launch

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds

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

  try {
    mainImpl(out)
  } 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")
}

private suspend fun mainImpl(out: Out) {
  val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
    runBlocking {
      out.info("${Emojis.CHECK_MARK} CoroutineExceptionHandler CALLED!")
      out.info("CoroutineExceptionHandler is called with throwable: ${throwable::class.simpleName} - ${throwable.message}")
    }
  }

  val parentJob = Job()
  val scope = CoroutineScope(parentJob + coroutineExceptionHandler)

  out.info("Launching coroutine that will throw exception...")
  val job = scope.launch {
    out.info("Launch coroutine started")

    out.delayLogsCancellation(500.milliseconds)

    throw MyExceptionWillThrowFromCoroutine.create("Exception from launch", out)
  }

  out.actionWithMsg("job.join()", { job.join() })
  out.infoPrintState(job, "launchJob")
  out.infoPrintState(scope, "scope-where-we-launched")

  scope.cancel()
}

class MyExceptionWillThrowFromCoroutine private constructor(msg: String) : RuntimeException(msg) {
  companion object {
    suspend fun create(msg: String, out: Out): MyExceptionWillThrowFromCoroutine {
      val exc = MyExceptionWillThrowFromCoroutine(msg)

      out.warn("${Emojis.EXCEPTOIN} throwing exception=[${exc::class.simpleName}] with msg=[${msg}]")

      return exc
    }
  }
}

Command to reproduce:

gt.sandbox.checkout.commit a7bc470832057f8c7ff6 \
&& 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:   20ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   40ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Launching coroutine that will throw exception...
[INFO][elapsed:   46ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Launch coroutine started
[INFO][elapsed:   46ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[job.join()]
[WARN][elapsed:  582ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] 💥  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[Exception from launch]
[INFO][elapsed:  584ms][2️⃣][⓷][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] ✅ CoroutineExceptionHandler CALLED!
[INFO][elapsed:  585ms][2️⃣][⓷][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] CoroutineExceptionHandler is called with throwable: MyExceptionWillThrowFromCoroutine - Exception from launch
[INFO][elapsed:  586ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[job.join()]
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [launchJob]:
launchJob.isActive    | false
launchJob.isCancelled | true
launchJob.isCompleted | true
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [scope-where-we-launched]: isActive=false
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

Unlike await() which does rethrow, join() just waits for co-routine to complete, without care of whether completion was successful or failure.

⚠️ join() does NOT rethrow code example ⚠️

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds

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

  try {
    mainImpl(out)
  } 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")
}

private suspend fun mainImpl(out: Out) {
  val parentJob = Job()
  val scope = CoroutineScope(parentJob)

  out.info("Launching coroutine that will throw exception...")
  val job = scope.launch {
    out.info("Launch coroutine started")

    out.delayLogsCancellation(500.milliseconds)

    throw MyExceptionWillThrowFromCoroutine.create("Exception from launch", out)
  }

  out.actionWithMsg("job.join()", { job.join() })
  out.infoPrintState(job, "launchJob")
  out.infoPrintState(scope, "scope-where-we-launched")
}

class MyExceptionWillThrowFromCoroutine private constructor(msg: String) : RuntimeException(msg) {
  companion object {
    suspend fun create(msg: String, out: Out): MyExceptionWillThrowFromCoroutine {
      val exc = MyExceptionWillThrowFromCoroutine(msg)

      out.warn("${Emojis.EXCEPTOIN} throwing exception=[${exc::class.simpleName}] with msg=[${msg}]")

      return exc
    }
  }
}

Command to reproduce:

gt.sandbox.checkout.commit 73693c5d8d2ddb27f9a7 \
&& 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:   29ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Launching coroutine that will throw exception...
[INFO][elapsed:   34ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Launch coroutine started
[INFO][elapsed:   34ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[job.join()]
[WARN][elapsed:  569ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] 💥  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[Exception from launch]
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: Exception from launch
	at com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine$Companion.create(Main.kt:46)
	at com.glassthought.sandbox.MainKt$mainImpl$job$1.invokeSuspend(Main.kt:34)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@e541635, Dispatchers.Default]
[INFO][elapsed:  576ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[job.join()]
[INFO][elapsed:  579ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [launchJob]:
launchJob.isActive    | false
launchJob.isCancelled | true
launchJob.isCompleted | true
[INFO][elapsed:  579ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [scope-where-we-launched]: isActive=false
[INFO][elapsed:  580ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

Also see

Here we cancel the parent scope and that automatically cancels children.

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

private val out = Out.standard()
private suspend fun mainImpl(out: Out) {
  coroutineScope {
    foo("msg-1")
  }
}

private suspend fun foo(msg: String) {
  out.actionWithMsg("fooImpl", { fooImpl(msg) })
}

private suspend fun fooImpl(msg: String) {
  coroutineScope {
    val deferred3 = async {
      out.delayNamed(3.seconds, "delayed([${msg}])")

      "res-3"
    }
    val deferred2 = async {
      out.delayNamed(2.seconds, "delayed([${msg}])")

      "res-2"
    }
    val deferred1 = async {
      out.delayNamed(1.seconds, "delayed([${msg}])")

      "res-1"
    }


    out.info("Just launched co-routines")
    out.delayNamed(500.milliseconds, "delay before scope cancel")
    this.cancel()

    out.info("deferred-1.result=" + deferred1.await())
    out.info("deferred-2.result=" + deferred2.await())
    out.info("deferred-3.result=" + deferred3.await())
  }

}

fun main(): Unit = runBlocking {
  out.info("START - ON MAIN")

  try {
    mainImpl(out)
  } catch (e: Exception) {
    out.error("back at 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")
}

Command to reproduce:

gt.sandbox.checkout.commit a2df237045869c0a8e1b \
&& 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:   39ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   56ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   58ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Just launched co-routines
[INFO][elapsed:   60ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Delaying for 500ms what_for=[delay before scope cancel]
[INFO][elapsed:   63ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   63ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   63ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed:  562ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Done delaying for 500ms what_for=[delay before scope cancel]
[WARN][elapsed:  615ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it 🫡
[WARN][elapsed:  615ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it 🫡
[WARN][elapsed:  616ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] 🫡 I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it 🫡
[INFO][elapsed:  616ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>][🫡] Cancellation Exception - rethrowing.
[ERROR][elapsed:  617ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] back at MAIN got an exception! of type=[JobCancellationException] with msg=[ScopeCoroutine was cancelled] cause=[kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job="coroutine#1":ScopeCoroutine{Cancelled}@768b970c]. 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 1s

Scope with Regular Job - throw CancellationException - stops co-routine that threw. Does NOT stop sibling co-routine, does not rethrow to parent.

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.system.exitProcess

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

  try {
    runBlocking {

      launch(CoroutineName("WillThrowCancelExc")) {
        // Loop over a range from 1 to 5 (inclusive)
        val howMany = 5
        for (i in 1..howMany) {
          val timeMillis = 1000L
          out.info("I will call throw CancellationException in $timeMillis ms - processing value:[${i}/${howMany}]")
          delay(timeMillis)
          out.warn("I am throwing CancellationException at value - [${i}/${howMany}]")

          throw CancellationException("cancel-message")
        }
      }

      launch(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
            }
          }

        out.info("${Emojis.CHECK_MARK} I have FINISHED all of my messages.")
      }
    }

  } catch (e: Exception) {
    out.error("runBlocking threw an exception! of type=[${e::class.simpleName}] with msg=[${e.message}]")

    exitProcess(1)
  }

  out.info("DONE no errors at main.")
}

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

Command to reproduce:

gt.sandbox.checkout.commit b7c3be031f6453aa7ce9 \
&& 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:   25ms][🥇][🧵][tname:main/tid:1] START
[INFO][elapsed:   67ms][🥇][①][coroutname:@WillThrowCancelExc#2][tname:main/tid:1] I will call throw CancellationException in 1000 ms - processing value:[1/5]
[INFO][elapsed:   74ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-0
[INFO][elapsed:  575ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-1
[WARN][elapsed: 1074ms][🥇][①][coroutname:@WillThrowCancelExc#2][tname:main/tid:1] I am throwing CancellationException at value - [1/5]
[INFO][elapsed: 1075ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-2
[INFO][elapsed: 1576ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-3
[INFO][elapsed: 2076ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-4
[INFO][elapsed: 2577ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-5
[INFO][elapsed: 3077ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-6
[INFO][elapsed: 3578ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-7
[INFO][elapsed: 4079ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-8
[INFO][elapsed: 4579ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-9
[INFO][elapsed: 5080ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-10
[INFO][elapsed: 5581ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] ✅ I have FINISHED all of my messages.
[INFO][elapsed: 5581ms][🥇][🧵][tname:main/tid:1] DONE no errors at main.

Calling this.cancel()

Scope with Regular Job - Call this.cancel() - Will stop co-routine on next cooperative cancelation function invocation. Does NOT stop sibling co-routine, does not rethrow to parent. ⚠️Will not stop right away⚠️

Highlight

When co-routine calls this.cancel() it does NOT stop processing right away, it stops processing once it reaches Cancellation Cooperative Functions/suspension point.

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.system.exitProcess

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

  out.info("START")

  try {
    runBlocking {

      launch(CoroutineName("WillCancelMyself")) {
        // Loop over a range from 1 to 5 (inclusive)
        val howMany = 5
        for (i in 1..howMany) {

          val timeMillis = 1000L
          out.info("I will call cancel in $timeMillis ms - processing value:[${i}/${howMany}] - going into delay()")

          try {
            delay(timeMillis)
          } 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.warn("I am calling this.cancel() at value - [${i}/${howMany}]")
          this.cancel()

          out.warn("${Emojis.WARNING_SIGN} We continued work after this.cancel()${Emojis.WARNING_SIGN}")

        }
      }

      launch(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
            }
          }

        out.info("${Emojis.CHECK_MARK} I have FINISHED all of my messages.")
      }
    }

  } catch (e: Exception) {
    out.error("runBlocking threw an exception! of type=[${e::class.simpleName}] with msg=[${e.message}]")

    exitProcess(1)
  }

  out.info("DONE no errors at main.")
}

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

Command to reproduce:

gt.sandbox.checkout.commit 8798f084905aee1fef60 \
&& 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:   17ms][🥇][🧵][tname:main/tid:1] START
[INFO][elapsed:   51ms][🥇][①][coroutname:@WillCancelMyself#2][tname:main/tid:1] I will call cancel in 1000 ms - processing value:[1/5] - going into delay()
[INFO][elapsed:   57ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-0
[INFO][elapsed:  558ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-1
[WARN][elapsed: 1057ms][🥇][①][coroutname:@WillCancelMyself#2][tname:main/tid:1] I am calling this.cancel() at value - [1/5]
[WARN][elapsed: 1058ms][🥇][①][coroutname:@WillCancelMyself#2][tname:main/tid:1] ⚠️ We continued work after this.cancel()⚠️
[INFO][elapsed: 1058ms][🥇][①][coroutname:@WillCancelMyself#2][tname:main/tid:1] I will call cancel in 1000 ms - processing value:[2/5] - going into delay()
[WARN][elapsed: 1094ms][🥇][①][coroutname:@WillCancelMyself#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/StandaloneCoroutine was cancelled], and rethrowing it 🫡 
[INFO][elapsed: 1094ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-2
[INFO][elapsed: 1594ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-3
[INFO][elapsed: 2095ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-4
[INFO][elapsed: 2595ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-5
[INFO][elapsed: 3096ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-6
[INFO][elapsed: 3596ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-7
[INFO][elapsed: 4097ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-8
[INFO][elapsed: 4598ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-9
[INFO][elapsed: 5098ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] a-10
[INFO][elapsed: 5600ms][🥇][⓶][coroutname:@JustPrints#3][tname:main/tid:1] ✅ I have FINISHED all of my messages.
[INFO][elapsed: 5600ms][🥇][🧵][tname:main/tid:1] DONE no errors at main.

Key Takeaway

Unless you explicitly use SupervisorJob or supervisorScope, you're working with a scope with regular job that follows structured concurrency principles where one child's failure affects the entire coroutine family.


Children
  1. Scope examples with Regular Job

Backlinks