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.sandboximport gt.sandbox.util.output.Outimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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.
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.
โ ๏ธ 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 beforeawait() 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.sandboximport com.glassthought.sandbox.util.out.impl.outimport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.asyncimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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
Newer behavior: exception propagates through Job link to parent scope WITHOUT going through await()
โ ๏ธ 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.sandboximport com.glassthought.sandbox.util.out.impl.outimport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.asyncimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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 cancel the parent scope and that automatically cancels children.
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.asyncimport kotlinx.coroutines.cancelimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.asyncimport kotlinx.coroutines.delayimport kotlinx.coroutines.isActiveimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.secondsfun 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.debugPicked 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 | falsecoRoutine1Deferred.isCancelled | truecoRoutine1Deferred.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 1FAILURE: 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 beforeawait() 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.sandboximport com.glassthought.sandbox.util.out.impl.outimport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.asyncimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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
Newer behavior: exception propagates through Job link to parent scope WITHOUT going through await()
โ ๏ธ 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.sandboximport com.glassthought.sandbox.util.out.impl.outimport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.asyncimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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
CoroutineExceptionHandler processes uncaught exception from launch
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsfun 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.debugPicked 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 | falselaunchJob.isCancelled | truelaunchJob.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
โ ๏ธ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.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsfun 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.debugPicked 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 | falsedeferredResult.isCancelled | truedeferredResult.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.sandboximport com.glassthought.sandbox.util.out.impl.outimport kotlinx.coroutines.*import kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsfun 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.debugPicked 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.sandboximport 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.debugPicked 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.
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.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsfun 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.debugPicked 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 | falselaunchJob.isCancelled | truelaunchJob.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
Here we cancel the parent scope and that automatically cancels children.
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.asyncimport kotlinx.coroutines.cancelimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsimport kotlin.time.Duration.Companion.secondsprivate 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.debugPicked 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 1FAILURE: 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.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcesssuspend 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.debugPicked 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.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcesssuspend 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.debugPicked 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.