Special exception thrown by cancellation-cooperative functions when a coroutine is cancelled.
Key feature: Designed to be handled gracefully by coroutine machinery for proper cancellation flow.
A coroutine that throws a CancellationException is considered to be cancelled normally.
If a different exception causes the cancellation, then the job has failed.
When a job has failed, its parent gets cancelled with the same type of exception, thus ensuring transparency in delegating parts of the job to its children. [unless parent has supervisor job] - kdoc
CancellationException does NOT propagate to its
parent.
If an exception is a subclass of CancellationException, it will not
be propagated to its parent. It will only cause cancellation of the
current coroutine. - kotlin-coroutines-deep-dive
When: the co-routine that called these functions has been cancelled.
Examples
CancellationException focused examples
cancel-parent-cancel-children
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.