In simple terms, a CoroutineScope in Kotlin is like a “container” that manages your coroutines. It defines the boundaries and the “lifetime” for the coroutines you start within it, ensuring that they are automatically canceled when they are no longer needed.

Job

Job Highlight

Job is the primary mechanism for controlling cancellation behavior of co-routines.

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

Exception/Cancellation Behavior with Regular Job

In ALL scopes that use Regular:

  • Uncaught, non-cancellation exception → Cancels Siblings + Propagates recursively through parent Job hierarchy, with each parent shutting down its children, Effectively shutting down the entire hierarchy up to the top most Job (or up to where you caught exception at error boundary).

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{max-width: 500px, display: block, margin: 0 auto, border: 5px solid black}

Creating Error Boundary

Gotchas

Cancellation Gotchas (With Regular Job)

Link to original

Link to original

Link to original

Job states

img{max-width: 500px, display: block, margin: 0 auto, border: 5px solid black}

In the “Active” state, a job is running and doing its job. If the job is created with a coroutine builder, this is the state where the body of this coroutine will be executed. In this state, we can start child coroutines. Most coroutines will start in the “Active” state. Only those that are started lazily will start with the “New” state. These need to be started in order for them to move to the “Active” state. When a coroutine is executing its body, it is surely in the “Active” state. When it is done, its state changes to “Completing”, where it waits for its children. Once all its children are done, the job changes its state to “Completed”, which is a terminal one. Alternatively, if a job cancels or fails when running (in the “Active” or “Completing” state), its state will change to “Cancelling”. In this state, we have the last chance to do some clean-up, like closing connections or freeing resources (we will see how to do this in the next chapter). Once this is done, the job will move to the “Cancelled” state.

StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse
Link to original

SupervisorJob

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.

SupervisorJob() examples

Straightforward Examples

Straightforward SupervisorJob 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
Link to original

Trickier examples

Trickier SupervisorJob() examples

+SupervisorJob()/+Job()

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

+Job()/+SupervisorJob() must-inherit-from-parent or it will ⚠️not respect parent cancellation⚠️

issue

Bug prone: +Job() Ignores parent cancellation

SupervisorJob() added at async level -> cancel entire scope -> job with SupervisorJob() does NOT care about scope cancellation and keeps going - ⚠️🐛⚠️

+SupervisorJob() is likely NOT what you want.

We have to be careful with creating a brand new SupervisorJob() since that UNLINKS it from the parent/child chain. Making it ignore cancellations coming from the parent. Example below shows the issue where co-routine created with + SupervisorJob() ignores cancellation of the parent scope.

After checking this problem look at the next example of how we can keep connection of parent -> child cancellations.

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.cancel
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("DoesNotCareAboutScopeCancellation") + 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 entire scope")
    delay(1500)
    out.info("Cancelling scope - ${Emojis.CANCELLATION}")
    scope.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 4c71bd4d11e9f563f1da \
&& 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:   35ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay 1500 before cancelling entire scope
[INFO][elapsed:   37ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-0
[INFO][elapsed:  539ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-1
[INFO][elapsed: 1040ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-2
[INFO][elapsed: 1537ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Cancelling scope - ❌
[INFO][elapsed: 1539ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay before deferred.await()
[INFO][elapsed: 1540ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-3
[INFO][elapsed: 2041ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-4
[INFO][elapsed: 2542ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-5
[INFO][elapsed: 3039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] calling deferred.await()
[INFO][elapsed: 3042ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-6
[INFO][elapsed: 3543ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-7
[INFO][elapsed: 4044ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-8
[INFO][elapsed: 4544ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-9
[INFO][elapsed: 5045ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-10
[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-1 result: kotlin.Unit
[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main

+Job() has the same issue of unlinking from parent cancellations

+Job() has the same issue of unlinking from parent cancellations - ⚠️🐛⚠️

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(): kotlin.Unit = runBlocking {
  val out = Out.standard()
  out.info("START")
 
  try {
    val scope = CoroutineScope(
      Dispatchers.Default
    )
 
    val deferred = scope.async(CoroutineName("DoesNotCareAboutScopeCancellation") + Job()) {
      (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
          }
 
        }
 
      "result-from-deferred"
    }
 
    out.delayedActionWithMsg(
      "cancelling scope",
      action = {
        scope.cancel()
      },
      delayBeforeAction = 1500.milliseconds
    )
 
    out.delayedActionWithMsg(
      "getting result from deferred",
      action = {
        out.info("co-routine-1 result: " + deferred.await())
      },
      delayBeforeAction = 1500.milliseconds
    )
  } 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 31674a09b0c92c89d928 \
&& 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:   37ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-0
[INFO][elapsed:   37ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [1.5sms delay] before_action=[cancelling scope]
[INFO][elapsed:  539ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-1
[INFO][elapsed: 1040ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-2
[INFO][elapsed: 1539ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [Done with=1.5sms delay] performing action=[cancelling scope]
[INFO][elapsed: 1540ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-3
[INFO][elapsed: 1541ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] performed action=[cancelling scope]
[INFO][elapsed: 1541ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [1.5sms delay] before_action=[getting result from deferred]
[INFO][elapsed: 2041ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-4
[INFO][elapsed: 2542ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-5
[INFO][elapsed: 3042ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [Done with=1.5sms delay] performing action=[getting result from deferred]
[INFO][elapsed: 3042ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-6
[INFO][elapsed: 3543ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-7
[INFO][elapsed: 4044ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-8
[INFO][elapsed: 4544ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-9
[INFO][elapsed: 5045ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-10
[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-1 result: result-from-deferred
[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] performed action=[getting result from deferred]
[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main
Link to original

fix

How to Fix the ignore of parent cancellation when creating new Jobs

Use +SupervisorJob(coroutineContext[Job]) OR +Job(coroutineContext[Job])

IF you need to create Job instance by hand THEN:

  • Use +SupervisorJob(coroutineContext[Job]) Instead of +SupervisorJob()
  • Use Use +Job(coroutineContext[Job]) Instead of +Job()
+ SupervisorJob(coroutineContext[Job]) respects the cancellation of parent scope - ✅

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.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds
 
fun main(): kotlin.Unit = runBlocking {
  val out = Out.standard()
  out.info("START")
 
  try {
    val scope = CoroutineScope(
      Dispatchers.Default
    )
 
    val deferredLevel1 = scope.launch(CoroutineName("level-1")) {
      coroutineScope {
 
        val isolatedScope = CoroutineScope(
          coroutineContext + SupervisorJob(coroutineContext[Job])
        )
 
        val deferred = isolatedScope.async(
          CoroutineName("Cares about cancellation of parent scope")
        ) {
          (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.delayedActionWithMsg(
          "cancel top level scope",
          action = {
            scope.cancel("Cancelling top level scope - ${Emojis.CANCELLATION}")
          },
          delayBeforeAction = 1500.milliseconds
        )
 
        out.delayedActionWithMsg(
          "await on deferred",
          action = {
            // This will throw CancellationException, since we cancelled the scope
            out.info("deferred result=" + deferred.await())
          },
          delayBeforeAction = 1500.milliseconds
        )
      }
    }
    deferredLevel1.join()
 
  } 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 5cc712f733a6345c8f05 \
&& 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:   36ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-0
[INFO][elapsed:   36ms][3️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-1/tid:31] [1.5sms delay] before_action=[cancel top level scope]
[INFO][elapsed:  537ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-1
[INFO][elapsed: 1038ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-2
[INFO][elapsed: 1537ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] [Done with=1.5sms delay] performing action=[cancel top level scope]
[INFO][elapsed: 1538ms][3️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-1/tid:31] a-3
[INFO][elapsed: 1540ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] performed action=[cancel top level scope]
[INFO][elapsed: 1540ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] [1.5sms delay] before_action=[await on deferred]
[WARN][elapsed: 1585ms][3️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-1/tid:31] 🫡 I have caught [CancellationException/Cancelling top level scope - �, and rethrowing it 🫡 
[INFO][elapsed: 1586ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main
Link to original

Link to original

Side note

You typically do NOT need to create Job()/SupervisorJob() by hand. Look at scope-function-error-boundary instead of creating Job()/SupervisorJob() by hand.

Link to original

Link to original

Link to original

Link to original

Link to original

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

Exception/Cancellation Behavior with Regular Job

In ALL scopes that use Regular:

  • Uncaught, non-cancellation exception → Cancels Siblings + Propagates recursively through parent Job hierarchy, with each parent shutting down its children, Effectively shutting down the entire hierarchy up to the top most Job (or up to where you caught exception at error boundary).

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{max-width: 500px, display: block, margin: 0 auto, border: 5px solid black}

Creating Error Boundary

Gotchas

Cancellation Gotchas (With Regular Job)

Link to original

Link to original

Code example

Scope examples with Regular Job

Core examples with regular Job

structured-parallelization

manually-with-scope with child parallelization launched co-routines

Here we manually setup scope and wait on it for launched actions to finish - a bit cumbersome but works.

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.seconds
 
private val out = Out.standard()
 
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")
}
 
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) {
  val job = Job()
  val scope = CoroutineScope(coroutineContext + job)
 
  scope.launch {
    out.delayNamed(3.seconds, "delayed([${msg}])")
  }
  scope.launch {
    out.delayNamed(2.seconds, "delayed([${msg}])")
  }
  scope.launch {
    out.delayNamed(1.seconds, "delayed([${msg}])")
  }
 
  out.actionWithMsg("job.complete()", { job.complete() })
  out.actionWithMsg("job.join()", { job.join() })
}

Command to reproduce:

gt.sandbox.checkout.commit 800315e25e0cc39077c2 \
&& 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:   37ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   52ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   54ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [>] Starting action=[job.complete()] 
[INFO][elapsed:   55ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [<] Finished action=[job.complete()].
[INFO][elapsed:   55ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [>] Starting action=[job.join()] 
[INFO][elapsed:   59ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   60ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   61ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1062ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 2061ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Done delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed: 3060ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Done delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed: 3061ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [<] Finished action=[job.join()].
[INFO][elapsed: 3061ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [<] Finished action=[fooImpl].
[INFO][elapsed: 3061ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on MAIN
Link to original

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

coroutineScope-happy-case

Here we use coroutineScope with 3 parallel children, coroutineScope will auto wait for them to finish.

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess
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 {
    launch {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(1.seconds, "delayed([${msg}])")
    }
  }
}
 
 
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 e09d077f5c59925b2881 \
&& 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:   54ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   60ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   62ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   62ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1064ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 2062ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Done delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed: 3061ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Done delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed: 3062ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [<] Finished action=[fooImpl].
[INFO][elapsed: 3062ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on MAIN
Link to original

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

throw-exception-in-one-which-cancels-others

Throw exception in one child co-routine which cancels others (as well as cancels parent)

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
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 {
    launch {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(1.seconds, "delayed([${msg}])")
      out.actionWithMsg(
        "throw-exception",
        { throw MyRuntimeException.create("from-launch-with-1sec-delay", out) })
 
    }
  }
}
 
 
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 115eb01d02706b26722e \
&& 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:   38ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   53ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   58ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   61ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   61ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1062ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1063ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] [>] Starting action=[throw-exception] 
[WARN][elapsed: 1102ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1]    💥 throwing exception=[MyRuntimeException] with msg=[from-launch-with-1sec-delay]
[WARN][elapsed: 1102ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] [<][💥] Finished action=[throw-exception], threw exception of type=[MyRuntimeException].
[WARN][elapsed: 1123ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed: 1124ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed: 1124ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [<][💥] Finished action=[fooImpl], threw exception of type=[MyRuntimeException].
[ERROR][elapsed: 1125ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] back at MAIN got an exception! of type=[MyRuntimeException] with msg=[from-launch-with-1sec-delay] cause=[null]. 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
Link to original

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

throw-cancellation-exception

One child throws cancellation exception which just cancels itself, allowing other SIBLING co-routines to finish processing without issues. Parent also finishes successfully in this case.

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
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 {
    launch {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(1.seconds, "delayed([${msg}])")
      out.info("Throwing cancellation exception")
      throw CancellationException("cancelled-1")
    }
  }
}
 
 
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 a9eae6cf8babc917214c \
&& 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:   55ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   59ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   62ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   62ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1063ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1063ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Throwing cancellation exception
[INFO][elapsed: 2063ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Done delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed: 3061ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Done delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed: 3062ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Finished action=[fooImpl].
[INFO][elapsed: 3062ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on MAIN
Link to original

With await:

happy-case

happy case we spawn 3 child awaits and get their results.

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
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.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 fedf1c8aed4a6059c3ee \
&& 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:   37ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   52ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   54ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Just launched co-routines
[INFO][elapsed:   58ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   59ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   59ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1061ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed: 1062ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    deferred-1.result=res-1
[INFO][elapsed: 2060ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Done delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed: 2060ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    deferred-2.result=res-2
[INFO][elapsed: 3059ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Done delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed: 3059ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    deferred-3.result=res-3
[INFO][elapsed: 3060ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Finished action=[fooImpl].
[INFO][elapsed: 3060ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on MAIN
Link to original

⚠️ Unhandled exception from async can go to parent scope without going through await() ⚠️

Uncaught exception behavior with async:

  • IF await() is called before the async coroutine fails.
    • THEN: the exception goes through await()
  • IF the async coroutine fails before await() is called.
    • THEN: the exception propagates up the Job hierarchy immediately.

As we would expect example: exception goes through await()

Here we observe await() receiving exception

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.async
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 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 {
    launch {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    val deferred = async {
      out.delayNamed(500.milliseconds, "delayed([${msg}])")
 
      throw MyRuntimeException.create("exception-from-async", out)
    }
    out.info("Just launched co-routines. Going into deferred.await()")
 
    // This code is unreachable since async exception will go to the parent instead of
    // going to the await()
    try {
      deferred.await()
    } catch (e: Exception) {
      out.error("[RETHROWING][${Emojis.CAUGHT_EXCEPTION}] Caught exception on [deferred.await()] from async: ${e.message} of type [${e::class.simpleName}]")
 
      throw e
    }
  }
}
 
 
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 fa1ebcc936a485d23912 \
&& 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 - ON MAIN
[INFO][elapsed:   31ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   34ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Just launched co-routines. Going into deferred.await()
[INFO][elapsed:   38ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 500ms what_for=[delayed([msg-1])]
[INFO][elapsed:  541ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 500ms what_for=[delayed([msg-1])]
[WARN][elapsed:  571ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] 💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async]
[WARN][elapsed:  591ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  592ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[ERROR][elapsed:  593ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [RETHROWING][� Caught exception on [deferred.await()] from async: exception-from-async of type [MyRuntimeException]
[WARN][elapsed:  593ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [<][💥] Finished action=[fooImpl], threw exception of type=[MyRuntimeException].
[ERROR][elapsed:  594ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] back at MAIN got an exception! of type=[MyRuntimeException] with msg=[exception-from-async] cause=[null]. 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
⚠️ Here we add delay() such that exception is thrown from async BEFORE we call await() on deferred object.
Result: Exception goes directly to parent scope without going through await(). This cancels any other co-routines on scope with regular Job(). ⚠️

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.async
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 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 {
    launch(CoroutineName("IJustDelay-3Sec-AndPrint")) {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch(CoroutineName("IJustDelay-2Sec-AndPrint")) {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    val deferred = async(CoroutineName("IAmGoingToThrow")) {
      out.delayNamed(500.milliseconds, "delayed([${msg}])")
 
      throw MyRuntimeException.create("exception-from-async", out)
    }
    out.info("Just launched co-routines.")
 
    out.delayNamed(1.seconds, "Going into DELAY before calling [deferred.await()]")
 
    // This code is unreachable since async exception will go to the parent instead of
    // going to the await()
    try {
      out.info("About to call [deferred.await()] (WE ARE NEVER GOING TO REACH THIS LINE)")
      deferred.await()
    } catch (e: Exception) {
      out.error("[RETHROWING][${Emojis.CAUGHT_EXCEPTION}] Caught exception on [deferred.await()] from async: ${e.message} of type [${e::class.simpleName}]")
 
      throw e
    }
  }
}
 
 
fun main(): Unit = runBlocking(CoroutineName("RunBlocking-At-Main")) {
  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 172df689c3cb592bc3eb \
&& 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:@RunBlocking-At-Main#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   32ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   35ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    Just launched co-routines.
[INFO][elapsed:   36ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    Delaying for 1s what_for=[Going into DELAY before calling [deferred.await()]]
[INFO][elapsed:   39ms][🥇][⓶][coroutname:@IJustDelay-3Sec-AndPrint#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓷][coroutname:@IJustDelay-2Sec-AndPrint#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   40ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] Delaying for 500ms what_for=[delayed([msg-1])]
[INFO][elapsed:  541ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] Done delaying for 500ms what_for=[delayed([msg-1])]
[WARN][elapsed:  572ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] 💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async]
[WARN][elapsed:  591ms][🥇][⓶][coroutname:@IJustDelay-3Sec-AndPrint#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  592ms][🥇][⓷][coroutname:@IJustDelay-2Sec-AndPrint#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  593ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    🫡 I have caught [JobCancellationException/ScopeCoroutine is cancelling], and rethrowing it 🫡
[WARN][elapsed:  594ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] [<][💥] Finished action=[fooImpl], threw exception of type=[MyRuntimeException].
[ERROR][elapsed:  594ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] back at MAIN got an exception! of type=[MyRuntimeException] with msg=[exception-from-async] cause=[null]. 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
Link to original

Cancellation of Parent Example

cancel-parent-cancel-children

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
Link to original

Link to original

run-blocking-examples

Link to original

custom-scope-with-async

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:

⚠️ Unhandled exception from async can go to parent scope without going through await() ⚠️

Uncaught exception behavior with async:

  • IF await() is called before the async coroutine fails.
    • THEN: the exception goes through await()
  • IF the async coroutine fails before await() is called.
    • THEN: the exception propagates up the Job hierarchy immediately.

As we would expect example: exception goes through await()

Here we observe await() receiving exception

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.async
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 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 {
    launch {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    val deferred = async {
      out.delayNamed(500.milliseconds, "delayed([${msg}])")
 
      throw MyRuntimeException.create("exception-from-async", out)
    }
    out.info("Just launched co-routines. Going into deferred.await()")
 
    // This code is unreachable since async exception will go to the parent instead of
    // going to the await()
    try {
      deferred.await()
    } catch (e: Exception) {
      out.error("[RETHROWING][${Emojis.CAUGHT_EXCEPTION}] Caught exception on [deferred.await()] from async: ${e.message} of type [${e::class.simpleName}]")
 
      throw e
    }
  }
}
 
 
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 fa1ebcc936a485d23912 \
&& 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 - ON MAIN
[INFO][elapsed:   31ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   34ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    Just launched co-routines. Going into deferred.await()
[INFO][elapsed:   38ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 500ms what_for=[delayed([msg-1])]
[INFO][elapsed:  541ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] Done delaying for 500ms what_for=[delayed([msg-1])]
[WARN][elapsed:  571ms][🥇][⓸][coroutname:@coroutine#4][tname:main/tid:1] 💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async]
[WARN][elapsed:  591ms][🥇][⓶][coroutname:@coroutine#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  592ms][🥇][⓷][coroutname:@coroutine#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[ERROR][elapsed:  593ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1]    [RETHROWING][� Caught exception on [deferred.await()] from async: exception-from-async of type [MyRuntimeException]
[WARN][elapsed:  593ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [<][💥] Finished action=[fooImpl], threw exception of type=[MyRuntimeException].
[ERROR][elapsed:  594ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] back at MAIN got an exception! of type=[MyRuntimeException] with msg=[exception-from-async] cause=[null]. 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
⚠️ Here we add delay() such that exception is thrown from async BEFORE we call await() on deferred object.
Result: Exception goes directly to parent scope without going through await(). This cancels any other co-routines on scope with regular Job(). ⚠️

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.async
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 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 {
    launch(CoroutineName("IJustDelay-3Sec-AndPrint")) {
      out.delayNamed(3.seconds, "delayed([${msg}])")
    }
    launch(CoroutineName("IJustDelay-2Sec-AndPrint")) {
      out.delayNamed(2.seconds, "delayed([${msg}])")
    }
    val deferred = async(CoroutineName("IAmGoingToThrow")) {
      out.delayNamed(500.milliseconds, "delayed([${msg}])")
 
      throw MyRuntimeException.create("exception-from-async", out)
    }
    out.info("Just launched co-routines.")
 
    out.delayNamed(1.seconds, "Going into DELAY before calling [deferred.await()]")
 
    // This code is unreachable since async exception will go to the parent instead of
    // going to the await()
    try {
      out.info("About to call [deferred.await()] (WE ARE NEVER GOING TO REACH THIS LINE)")
      deferred.await()
    } catch (e: Exception) {
      out.error("[RETHROWING][${Emojis.CAUGHT_EXCEPTION}] Caught exception on [deferred.await()] from async: ${e.message} of type [${e::class.simpleName}]")
 
      throw e
    }
  }
}
 
 
fun main(): Unit = runBlocking(CoroutineName("RunBlocking-At-Main")) {
  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 172df689c3cb592bc3eb \
&& 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:@RunBlocking-At-Main#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   32ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   35ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    Just launched co-routines.
[INFO][elapsed:   36ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    Delaying for 1s what_for=[Going into DELAY before calling [deferred.await()]]
[INFO][elapsed:   39ms][🥇][⓶][coroutname:@IJustDelay-3Sec-AndPrint#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   39ms][🥇][⓷][coroutname:@IJustDelay-2Sec-AndPrint#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   40ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] Delaying for 500ms what_for=[delayed([msg-1])]
[INFO][elapsed:  541ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] Done delaying for 500ms what_for=[delayed([msg-1])]
[WARN][elapsed:  572ms][🥇][⓸][coroutname:@IAmGoingToThrow#4][tname:main/tid:1] 💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async]
[WARN][elapsed:  591ms][🥇][⓶][coroutname:@IJustDelay-3Sec-AndPrint#2][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  592ms][🥇][⓷][coroutname:@IJustDelay-2Sec-AndPrint#3][tname:main/tid:1] 🫡 I have caught [JobCancellationException/Parent job is Cancelling], and rethrowing it 🫡
[WARN][elapsed:  593ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1]    🫡 I have caught [JobCancellationException/ScopeCoroutine is cancelling], and rethrowing it 🫡
[WARN][elapsed:  594ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] [<][💥] Finished action=[fooImpl], threw exception of type=[MyRuntimeException].
[ERROR][elapsed:  594ms][🥇][①][coroutname:@RunBlocking-At-Main#1][tname:main/tid:1] back at MAIN got an exception! of type=[MyRuntimeException] with msg=[exception-from-async] cause=[null]. 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
Link to original

Link to original

CoroutineExceptionHandler

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

Highlights

Examples

CoroutineExceptionHandler only 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
Link to original

⚠️CoroutineExceptionHandler: does NOT work with async⚠️

⚠️ async & CoroutineExceptionHandler: CoroutineExceptionHandler with async will be ignored. AWAIT rethrows the exception - ⚠️

Doc

A coroutine that was created using async always catches all its exceptions and represents them in the resulting Deferred object, so it cannot result in uncaught exceptions. - kdoc

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) {
 
  // CoroutineExceptionHandler - will be IGNORED with async - ⚠️🐛⚠️
  // This handler will NEVER be called because async doesn't use it
  val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
    // This block will NEVER execute with async
    println("❌ CoroutineExceptionHandler called (this won't happen): ${throwable.message}")
  }
 
  // Create a scope with the exception handler
  // Note: Using just coroutineExceptionHandler for clarity (no SupervisorJob needed for this demo)
  val scope = CoroutineScope(coroutineExceptionHandler)
 
  out.info("Creating async coroutine...")
 
  val delayInChild = 500.milliseconds
  val deferredResult = scope.async {
    out.info("Async coroutine started, will throw exception after delay...")
    delay(delayInChild)
    throw MyExceptionWillThrowFromCoroutine.create("async-exception-msg", out)
  }
 
  out.info("${Emojis.BUG}: CoroutineExceptionHandler is installed but will NEVER be called for async")
  out.info("Reason: async stores exceptions until await() is called, doesn't propagate to handler")
 
  // Give async time to complete and throw internally
  out.delayNamed(delayInChild * 2, "giving async time to complete and store exception")
 
  // The exception is stored in the Deferred, not propagated to handler
  out.info("Async has completed. Exception is stored in Deferred.")
  out.infoPrintState(deferredResult, "deferredResult")
 
  // This will rethrow the stored exception to the caller
  out.info("Calling await() - this will rethrow the stored exception...")
  try {
    deferredResult.await()
    out.error("Should never reach here - await() should throw")
  } catch (e: Exception) {
    out.warn("✅ Exception caught by try-catch (NOT by CoroutineExceptionHandler): ${e.message}")
    out.info("This shows CoroutineExceptionHandler doesn't work with async/await")
  }
 
  out.info("Demonstration complete - handler was never called")
}
 
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 75b9c9b740f35ad9b8b7 \
&& 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:   30ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Creating async coroutine...
[INFO][elapsed:   36ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Async coroutine started, will throw exception after delay...
[INFO][elapsed:   36ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] ⚠️�oroutineExceptionHandler is installed but will NEVER be called for async
[INFO][elapsed:   36ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Reason: async stores exceptions until await() is called, doesn't propagate to handler
[INFO][elapsed:   38ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Delaying for 1s what_for=[giving async time to complete and store exception]
[WARN][elapsed:  560ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] 💥  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[async-exception-msg]
[INFO][elapsed: 1039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Done delaying for 1s what_for=[giving async time to complete and store exception]
[INFO][elapsed: 1039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Async has completed. Exception is stored in Deferred.
[INFO][elapsed: 1044ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [deferredResult]:
deferredResult.isActive    | false
deferredResult.isCancelled | true
deferredResult.isCompleted | true
 
[INFO][elapsed: 1045ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Calling await() - this will rethrow the stored exception...
[WARN][elapsed: 1048ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] ✅ Exception caught by try-catch (NOT by CoroutineExceptionHandler): async-exception-msg
[INFO][elapsed: 1048ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] This shows CoroutineExceptionHandler doesn't work with async/await
[INFO][elapsed: 1048ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Demonstration complete - handler was never called
[INFO][elapsed: 1048ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

⚠️ IF async throws unhandled exception before await THEN exception is lost⚠️

If async throws before await: The exception is lost and is NOT processed by CoroutineExceptionHandler

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
 
 
fun main(): Unit {
  val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
    runBlocking(CoroutineName("CoroutineExceptionHandler")) {
      out.error("Caught exception in CoroutineExceptionHandler: ${ex::class.simpleName} with message=[${ex.message}].")
    }
  }
 
  val mainJob = Job()
  val scope = CoroutineScope(mainJob + coroutineExceptionHandler)
 
  scope.launch(CoroutineName("SpawnedFromMain")) {
    out.actionWithMsg("foo", { foo(scope) })
  }
 
  runBlocking {
    out.actionWithMsg("mainJob.join()", { mainJob.join() })
  }
}
 
private suspend fun foo(scope: CoroutineScope) {
  val deferred = scope.async(CoroutineName("async-1")) {
    out.actionWithMsg("throw-exception", {
      throw MyRuntimeException.create("exception-from-async-before-it-got-to-await", out)
    }, delayDuration = 500.milliseconds)
  }
 
  out.info("Just launched co-routines.")
  out.actionWithMsg(
    "deferred.await()",
    {
      deferred.await()
    },
    delayDuration = 1.seconds
  )
}

Command to reproduce:

gt.sandbox.checkout.commit 0423eb04f6570cb822dd \
&& 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:   19ms][①][coroutname:@SpawnedFromMain#1] [->] action=[foo] is starting.
[INFO][elapsed:   19ms][⓶][coroutname:@coroutine#2] [->] action=[mainJob.join()] is starting.
[INFO][elapsed:   32ms][①][coroutname:@SpawnedFromMain#1]    Just launched co-routines.
[INFO][elapsed:   36ms][①][coroutname:@SpawnedFromMain#1]    [🐢] action=[deferred.await()] is being delayed for=[1000 ms] before starting.
[INFO][elapsed:   36ms][⓷][coroutname:@async-1#3] [🐢] action=[throw-exception] is being delayed for=[500 ms] before starting.
[INFO][elapsed:  536ms][⓷][coroutname:@async-1#3] [->] action=[throw-exception] is starting.
[WARN][elapsed:  566ms][⓷][coroutname:@async-1#3]    💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async-before-it-got-to-await]
[WARN][elapsed:  567ms][⓷][coroutname:@async-1#3] [<-][💥] Finished action=[throw-exception], it THREW exception of type=[MyRuntimeException] we are rethrowing it.
[INFO][elapsed:  583ms][①][coroutname:@SpawnedFromMain#1] [<-][🫡] Cancellation Exception - rethrowing.
[INFO][elapsed:  583ms][⓶][coroutname:@coroutine#2] [<-] Finished action=[mainJob.join()].
GT-Sandbox-Snapshot: Example where awaiting co-routine got cancelled before getting to await

Code

package com.glassthought.sandbox
 
import kotlinx.coroutines.*
 
val startMillis = System.currentTimeMillis()
 
 
fun myPrint(msg: String) {
  val elapsed = System.currentTimeMillis() - startMillis
 
  println("[${elapsed.toString().padStart(3)} ms] $msg")
}
 
fun main() {
  myPrint("[MAIN] START...")
 
  val mainScopeJob = Job()
  val mainScope = CoroutineScope(
    mainScopeJob
      + Dispatchers.IO
      + CoroutineExceptionHandler { _, throwable -> myPrint("👍 CoroutineExceptionHandler invoked exc.message=[${throwable.message}]👍 ") }
      + CoroutineName("main-scope")
  )
 
  val async = mainScope.async(CoroutineName("async-coroutine")) {
    myPrint(" [async-coroutine]: I am going to throw in about 100ms")
    delay(100)
    myPrint(" [async-coroutine]: Throwing exception now!")
 
    throw RuntimeException("I am an exception from async coroutine")
  }
 
  val waiter = mainScope.launch {
    try {
      myPrint("   [launch-routine-just-waiting] I am going to wait 500 millis and then await")
      delay(500)
      async.await()
    } catch (e: CancellationException) {
      myPrint("   [launch-routine-just-waiting] I was cancelled! CancellationException.message=[${e.message}]")
      throw e
    } catch (e: Exception) {
      myPrint("   [launch-routine-just-waiting] ✅ I caught an exception! Exception.message=[${e.message}]")
    }
  }
 
 
  runBlocking {
    mainScopeJob.join()
  }
 
  myPrint("[MAIN] DONE.")
}

Command to reproduce:

gt.sandbox.checkout.commit 1b939780478f3d10a020 \
&& 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
[  7 ms] [MAIN] START...
[ 43 ms]    [launch-routine-just-waiting] I am going to wait 500 millis and then await
[ 43 ms]  [async-coroutine]: I am going to throw in about 100ms
[145 ms]  [async-coroutine]: Throwing exception now!
[167 ms]    [launch-routine-just-waiting] I was cancelled! CancellationException.message=[Parent job is Cancelling]
[168 ms] [MAIN] DONE.
Link to original

Link to original

Gotchas:

CoroutineExceptionHandler gotchas

Link to original

Link to original

⚠️ join() does NOT rethrow ⚠️

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
Link to original

Also see

Link to original

CancellationException focused examples

cancel-parent-cancel-children

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
Link to original

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()

⚠️ cancel() call stops on next suspension point, NOT right away ⚠️

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 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.
Link to original

Link to original

Link to original

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.

Link to original

example

GT-Sandbox-Snapshot: Example - 1, Cancelling using SupervisorJob

Code

package com.glassthought.sandbox
 
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
 
interface Server {
  suspend fun start()
  suspend fun stop()
}
 
val out = Out.standard()
 
class ServerImpl(
  private val scope: CoroutineScope // Injected scope for better control
) : Server {
 
  private val job = SupervisorJob() // Ensures independent coroutines
  private val serverScope = scope + job
 
  override suspend fun start() {
    out.info("Starting server")
 
    serverScope.launch(CoroutineName("ServerWork-1")) {
      out.info("Running server work in thread: ${Thread.currentThread().name}")
      delay(2000)
      out.info("ServerWork-1 completed")
    }
 
    serverScope.launch(CoroutineName("ServerWork-2")) {
      out.info("Running additional server work in thread: ${Thread.currentThread().name}")
      delay(1500)
      out.info("ServerWork-2  completed")
    }
  }
 
  override suspend fun stop() {
    out.info("Stopping server")
 
    job.cancel()
  }
}
 
fun main() = runBlocking {
  out.info("--------------------------------------------------------------------------------")
  out.info("Example where the server is aborted prior to finishing:")
  runWithDelayBeforeStopping(1000)
 
  out.info("--------------------------------------------------------------------------------")
  out.info("Example where the server has time to finish:")
  runWithDelayBeforeStopping(3000)
}
 
private suspend fun runWithDelayBeforeStopping(delayBeforeStopping: Long) {
  val server = ServerImpl(CoroutineScope(Dispatchers.Default)) // Inject external scope
  server.start()
  delay(delayBeforeStopping) // Use delay instead of Thread.sleep
  server.stop()
}

Command to reproduce:

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

Recorded output of command:

[elapsed:   51ms][🥇/tname:main/tid:1][coroutine:unnamed] --------------------------------------------------------------------------------
[elapsed:   62ms][🥇/tname:main/tid:1][coroutine:unnamed] Example where the server is aborted prior to finishing:
[elapsed:   65ms][🥇/tname:main/tid:1][coroutine:unnamed] Starting server
[elapsed:   73ms][⓶/tname:DefaultDispatcher-worker-1/tid:20][coroutine:ServerWork-1] Running server work in thread: DefaultDispatcher-worker-1
[elapsed:   74ms][⓷/tname:DefaultDispatcher-worker-2/tid:21][coroutine:ServerWork-2] Running additional server work in thread: DefaultDispatcher-worker-2
[elapsed: 1084ms][🥇/tname:main/tid:1][coroutine:unnamed] Stopping server
[elapsed: 1087ms][🥇/tname:main/tid:1][coroutine:unnamed] --------------------------------------------------------------------------------
[elapsed: 1087ms][🥇/tname:main/tid:1][coroutine:unnamed] Example where the server has time to finish:
[elapsed: 1087ms][🥇/tname:main/tid:1][coroutine:unnamed] Starting server
[elapsed: 1088ms][⓶/tname:DefaultDispatcher-worker-1/tid:20][coroutine:ServerWork-1] Running server work in thread: DefaultDispatcher-worker-1
[elapsed: 1088ms][⓷/tname:DefaultDispatcher-worker-2/tid:21][coroutine:ServerWork-2] Running additional server work in thread: DefaultDispatcher-worker-2
[elapsed: 2594ms][⓷/tname:DefaultDispatcher-worker-2/tid:21][coroutine:ServerWork-2] ServerWork-2  completed
[elapsed: 3093ms][⓷/tname:DefaultDispatcher-worker-2/tid:21][coroutine:ServerWork-1] ServerWork-1 completed
[elapsed: 4089ms][🥇/tname:main/tid:1][coroutine:unnamed] Stopping server
Cannot re-use scope

Cannot Re Use Scope

https://chatgpt.com/share/31be55da-2e7f-4dc8-9fc3-000384f58086

Sample - 1
import kotlinx.coroutines.*
 
fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
 
    val job = scope.launch {
        println("Job started")
        delay(1000)
        println("Job completed")
    }
 
    delay(500)  // Wait for a while
    scope.cancel()  // Cancel the scope
 
    // Attempt to launch a new job in the cancelled scope
    val newJob = scope.launch {
        println("New job started")
        delay(1000)
        println("New job completed")
    }
 
    newJob.invokeOnCompletion { exception ->
        if (exception is CancellationException) {
            println("New job was cancelled due to scope cancellation")
        } else if (exception != null) {
            println("New job completed with exception: $exception")
        } else {
            println("New job completed successfully")
        }
    }
 
    // Wait for all jobs to complete
    joinAll(job, newJob)
}
Sample - 2
import kotlinx.coroutines.*
 
fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
 
    val job = scope.launch {
        println("Job started")
        delay(1000)
        println("Job completed")
    }
 
    delay(500)  // Wait for a while
    scope.cancel()  // Cancel the scope
 
    // Create a new scope and launch a new job
    val newScope = CoroutineScope(Dispatchers.Default)
    val newJob = newScope.launch {
        println("New job started")
        delay(1000)
        println("New job completed")
    }
 
    // Wait for all jobs to complete
    joinAll(job, newJob)
}
Link to original

Link to original

Scope functions

Coroutine scope function and Error Boundary

coroutineScope: The hero of coroutines.

coroutineScope creates a boundary that establishes a new CoroutineScope.

The new scope inherits its coroutineContext from the outer scope, but overrides the context’s Job. It also causes the current coroutine to suspend until all child coroutines have finished their execution.

The scope created by coroutineScope has a new Job - this creates an exception boundary where failures surface. However, exceptions will continue propagating up the Job hierarchy unless intercepted by exception handling (try-catch, CoroutineExceptionHandler) at the scope boundary.

Coroutine scope functions create an error/exception boundary: Allow us to try-catch the Exception to prevent it from bubbling up to parent Job.

Error boundary example

Without error boundary

launch { // Job A
    launch { // Job B - direct child of A
        throw Exception() // Exception happens in B
                         // B fails and cancels A
                         // NO stopping point at B
                         // Cancels A
    }
}

Moving towards error boundary using coroutineScope

launch { // Job A  
    coroutineScope { // Job B - creates exception boundary
        launch { // Job C - child of B
            throw Exception() // Exception happens in C
                              // C fails, B fails and surfaces exception
                              // CAN be caught at B's boundary (not caught right now)
        }
    }
}

Example creating error boundary try-catch:

launch {
   try {
       // coroutineScope will wait for all the child co-routines to finish.
       coroutineScope {
           launch { 
               delay(1000)
               throw RuntimeException("boom!") 
           }
       }
   } catch (e: CancellationException) {
       // Re-throw CancellationException - do NOT suppress cancellation!
       throw e
   } catch (e: Exception) {
      println("Caught: ${e.message}")
       // Handle other exceptions - stops propagation
       // Coroutine continues normally here
   }
   
   println("Launch continues after exception handling")
}

Details on individual Scope functions:

Coroutine scope function instances

coroutineScope (Function)

pseudo-code to illustrate what coroutineScope

// This is pseudo-code for purposes of understanding !!NOT TO BE USED AS IS!!
suspend fun <T> coroutineScope(block: suspend CoroutineScope.() -> T): T {
    // Get the current coroutine context
    val currentContext = coroutineContext
    
    // Create a new Job that's a child of the current job
    val scopeJob = Job(parent = currentContext[Job])
    
    // Create a new scope with the new job
    val scope = CoroutineScope(currentContext + scopeJob)
    
    try {
        // Execute the block and get result
        val result = block(scope)
        
        // This conceptually "waits" for all children to complete
        // (In reality this would need to be done differently)
        scopeJob.complete()  // Signal no more children will be added
        scopeJob.join()      // Wait for existing children
        
        return result
    } catch (e: Exception) {
        // Cancel all children on any exception
        scopeJob.cancelChildren()
        throw e
    }
}
Link to original

Link to original

withTimeout()

Another function that behaves a lot like coroutineScope is withTimeout. It also creates a scope and returns a value. Actually, withTimeout with a very big timeout behaves just like coroutineScope. The difference is that withTimeout additionally sets a time limit for its body execution. If it takes too long, it cancels this body and throws TimeoutCancellationException (a subtype of CancellationException).

Beware that withTimeout throws TimeoutCancellationException, which is a subtype of CancellationException (the same exception that is thrown when a coroutine is cancelled). So, when this exception is thrown in a coroutine builder, it only cancels it and does not affect its parent (as explained in the previous chapter). - kotlin-coroutines-deep-dive

Relationships

Gotchas

withTimeout gotchas

Link to original

Link to original

withContext

The withContext function is similar to coroutineScope, but it additionally allows some changes to be made to the scope. The CoroutineContext provided as an argument to this function overrides the context from the parent scope (the same way as in coroutine builders). This means that withContext(EmptyCoroutineContext) and coroutineScope() behave in exactly the same way.

The function withContext is often used to set a different coroutine scope for part of our code. Usually, you should use it together with Dispatcher. - kotlin-coroutines-deep-dive

Relationships

Gotchas

Link to original

supervisorScope

The supervisorScope function also behaves a lot like coroutineScope: it creates a CoroutineScope that inherits from the outer scope and calls the specified suspend block in it. The difference is that it overrides the context’s Job with Supervisor-Job, so it is not cancelled when a child raises an exception.

supervisorScope is mainly used in functions that start multiple independent tasks. - kotlin-coroutines-deep-dive

Highlight

  • Does NOT cancel when child raises an exception.
  • Does NOT cancel children when child raises an exception.
  • Waits for ALL children, even failing ones, even after some have failed and threw.
  • Collects ALL exceptions from co-routines.

Notes

Link to original

Link to original

Notes

nesting-is-allowed

If you need to use functionalities from two coroutine scope func- tions, you need to use one inside another. For instance, to set both a timeout and a dispatcher, you can use withTimeoutOrNull inside withContext. - kotlin-coroutines-deep-dive

suspend fun calculateAnswerOrNull(): User? =
    withContext(Dispatchers.Default) {
        withTimeoutOrNull(1000) {
            calculateAnswer()
        }
}
Link to original

Link to original

Building scope gotchas

building-scope-gotchas

Link to original