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