Scope examples with Regular Job

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

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

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

Next let's see how the cancellations are handled, we expect any single failure to cancel all 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

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

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

With await:

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

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() โš ๏ธ

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

Cancellation of Parent Example

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

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:

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() โš ๏ธ

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

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

Highlights

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

CoroutineExceptionHandler does NOT work with async

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 fails before await

If async fails before await: The exception appears to be lost and still 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()].

Filed github issue on this

Documentation needs adjustments: states async cannot cause uncaught exceptions but async can if fails before it is awaited. ยท Issue #4504 ยท Kotlin/kotlinx.coroutines

CoroutineExceptionHandler works with launch

CoroutineExceptionHandler processes uncaught exception from launch

Code

package com.glassthought.sandbox

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

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

  try {
    mainImpl(out)
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

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

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

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

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

    out.delayLogsCancellation(500.milliseconds)

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

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

  scope.cancel()
}

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

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

      return exc
    }
  }
}

Command to reproduce:

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

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   20ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   40ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Launching coroutine that will throw exception...
[INFO][elapsed:   46ms][2๏ธโƒฃ][โ“ถ][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Launch coroutine started
[INFO][elapsed:   46ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[job.join()]
[WARN][elapsed:  582ms][2๏ธโƒฃ][โ“ถ][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] ๐Ÿ’ฅ  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[Exception from launch]
[INFO][elapsed:  584ms][2๏ธโƒฃ][โ“ท][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] โœ… CoroutineExceptionHandler CALLED!
[INFO][elapsed:  585ms][2๏ธโƒฃ][โ“ท][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] CoroutineExceptionHandler is called with throwable: MyExceptionWillThrowFromCoroutine - Exception from launch
[INFO][elapsed:  586ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[job.join()]
[INFO][elapsed:  589ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] State of [launchJob]:
launchJob.isActive    | false
launchJob.isCancelled | true
launchJob.isCompleted | true
[INFO][elapsed:  589ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] State of [scope-where-we-launched]: isActive=false
[INFO][elapsed:  589ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

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

โš ๏ธ join() does NOT rethrow code example โš ๏ธ

Code

package com.glassthought.sandbox

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

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

  try {
    mainImpl(out)
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

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

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

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

    out.delayLogsCancellation(500.milliseconds)

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

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

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

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

      return exc
    }
  }
}

Command to reproduce:

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

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   14ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   29ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Launching coroutine that will throw exception...
[INFO][elapsed:   34ms][2๏ธโƒฃ][โ“ถ][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Launch coroutine started
[INFO][elapsed:   34ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[job.join()]
[WARN][elapsed:  569ms][2๏ธโƒฃ][โ“ถ][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] ๐Ÿ’ฅ  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[Exception from launch]
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: Exception from launch
	at com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine$Companion.create(Main.kt:46)
	at com.glassthought.sandbox.MainKt$mainImpl$job$1.invokeSuspend(Main.kt:34)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@e541635, Dispatchers.Default]
[INFO][elapsed:  576ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[job.join()]
[INFO][elapsed:  579ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] State of [launchJob]:
launchJob.isActive    | false
launchJob.isCancelled | true
launchJob.isCompleted | true
[INFO][elapsed:  579ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] State of [scope-where-we-launched]: isActive=false
[INFO][elapsed:  580ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

Also see

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

Code

package com.glassthought.sandbox

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

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

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

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

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

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

      "res-1"
    }


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

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

}

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

  try {
    mainImpl(out)
  } catch (e: Exception) {
    out.error("back at MAIN got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

  out.info("DONE - WITHOUT errors on MAIN")
}

Command to reproduce:

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

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   39ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] START - ON MAIN
[INFO][elapsed:   56ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] [>] Starting action=[fooImpl] 
[INFO][elapsed:   58ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1]    Just launched co-routines
[INFO][elapsed:   60ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1]    Delaying for 500ms what_for=[delay before scope cancel]
[INFO][elapsed:   63ms][๐Ÿฅ‡][โ“ถ][coroutname:@coroutine#2][tname:main/tid:1] Delaying for 3s what_for=[delayed([msg-1])]
[INFO][elapsed:   63ms][๐Ÿฅ‡][โ“ท][coroutname:@coroutine#3][tname:main/tid:1] Delaying for 2s what_for=[delayed([msg-1])]
[INFO][elapsed:   63ms][๐Ÿฅ‡][โ“ธ][coroutname:@coroutine#4][tname:main/tid:1] Delaying for 1s what_for=[delayed([msg-1])]
[INFO][elapsed:  562ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1]    Done delaying for 500ms what_for=[delay before scope cancel]
[WARN][elapsed:  615ms][๐Ÿฅ‡][โ“ถ][coroutname:@coroutine#2][tname:main/tid:1] ๐Ÿซก I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it ๐Ÿซก
[WARN][elapsed:  615ms][๐Ÿฅ‡][โ“ท][coroutname:@coroutine#3][tname:main/tid:1] ๐Ÿซก I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it ๐Ÿซก
[WARN][elapsed:  616ms][๐Ÿฅ‡][โ“ธ][coroutname:@coroutine#4][tname:main/tid:1] ๐Ÿซก I have caught [JobCancellationException/ScopeCoroutine was cancelled], and rethrowing it ๐Ÿซก
[INFO][elapsed:  616ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] [>][๐Ÿซก] Cancellation Exception - rethrowing.
[ERROR][elapsed:  617ms][๐Ÿฅ‡][โ‘ ][coroutname:@coroutine#1][tname:main/tid:1] back at MAIN got an exception! of type=[JobCancellationException] with msg=[ScopeCoroutine was cancelled] cause=[kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job="coroutine#1":ScopeCoroutine{Cancelled}@768b970c]. Exiting with error code 1

FAILURE: Build failed with an exception.

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

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

BUILD FAILED in 1s

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

Code

package com.glassthought.sandbox

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

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

  try {
    runBlocking {

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

          throw CancellationException("cancel-message")
        }
      }

      launch(CoroutineName("JustPrints")) {
        (0..10)
          .map { "a-${it}" }
          .forEach {
            out.info(it)

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

              throw e
            }
          }

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

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

    exitProcess(1)
  }

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

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

Command to reproduce:

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

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   25ms][๐Ÿฅ‡][๐Ÿงต][tname:main/tid:1] START
[INFO][elapsed:   67ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillThrowCancelExc#2][tname:main/tid:1] I will call throw CancellationException in 1000 ms - processing value:[1/5]
[INFO][elapsed:   74ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-0
[INFO][elapsed:  575ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-1
[WARN][elapsed: 1074ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillThrowCancelExc#2][tname:main/tid:1] I am throwing CancellationException at value - [1/5]
[INFO][elapsed: 1075ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-2
[INFO][elapsed: 1576ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-3
[INFO][elapsed: 2076ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-4
[INFO][elapsed: 2577ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-5
[INFO][elapsed: 3077ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-6
[INFO][elapsed: 3578ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-7
[INFO][elapsed: 4079ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-8
[INFO][elapsed: 4579ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-9
[INFO][elapsed: 5080ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-10
[INFO][elapsed: 5581ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] โœ… I have FINISHED all of my messages.
[INFO][elapsed: 5581ms][๐Ÿฅ‡][๐Ÿงต][tname:main/tid:1] DONE no errors at main.

Calling this.cancel()

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

Highlight

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

Code

package com.glassthought.sandbox

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

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

  out.info("START")

  try {
    runBlocking {

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

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

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

          out.warn("I am calling this.cancel() at value - [${i}/${howMany}]")
          this.cancel()

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

        }
      }

      launch(CoroutineName("JustPrints")) {
        (0..10)
          .map { "a-${it}" }
          .forEach {
            out.info(it)

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

              throw e
            }
          }

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

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

    exitProcess(1)
  }

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

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

Command to reproduce:

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

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   17ms][๐Ÿฅ‡][๐Ÿงต][tname:main/tid:1] START
[INFO][elapsed:   51ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillCancelMyself#2][tname:main/tid:1] I will call cancel in 1000 ms - processing value:[1/5] - going into delay()
[INFO][elapsed:   57ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-0
[INFO][elapsed:  558ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-1
[WARN][elapsed: 1057ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillCancelMyself#2][tname:main/tid:1] I am calling this.cancel() at value - [1/5]
[WARN][elapsed: 1058ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillCancelMyself#2][tname:main/tid:1] โš ๏ธ We continued work after this.cancel()โš ๏ธ
[INFO][elapsed: 1058ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillCancelMyself#2][tname:main/tid:1] I will call cancel in 1000 ms - processing value:[2/5] - going into delay()
[WARN][elapsed: 1094ms][๐Ÿฅ‡][โ‘ ][coroutname:@WillCancelMyself#2][tname:main/tid:1] ๐Ÿซก I have caught [JobCancellationException/StandaloneCoroutine was cancelled], and rethrowing it ๐Ÿซก 
[INFO][elapsed: 1094ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-2
[INFO][elapsed: 1594ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-3
[INFO][elapsed: 2095ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-4
[INFO][elapsed: 2595ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-5
[INFO][elapsed: 3096ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-6
[INFO][elapsed: 3596ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-7
[INFO][elapsed: 4097ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-8
[INFO][elapsed: 4598ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-9
[INFO][elapsed: 5098ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] a-10
[INFO][elapsed: 5600ms][๐Ÿฅ‡][โ“ถ][coroutname:@JustPrints#3][tname:main/tid:1] โœ… I have FINISHED all of my messages.
[INFO][elapsed: 5600ms][๐Ÿฅ‡][๐Ÿงต][tname:main/tid:1] DONE no errors at main.


Children
  1. Core examples with regular Job

Backlinks