From b0b5f60c4ec872eacdc439d41e092c9fc8eccea1 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 20 Jun 2026 00:22:58 +0100 Subject: [PATCH 1/7] Deprioritize cancellation in mergeSuppressed --- .../commonTest/kotlin/arrow/AutoCloseTest.kt | 114 ++++++++++++++---- .../kotlin/arrow/core/ThrowableUtils.kt | 6 + .../arrow/fx/coroutines/ResourceTest.kt | 6 +- 3 files changed, 101 insertions(+), 25 deletions(-) diff --git a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt index b68e8ad1fbb..d4fff6478ba 100644 --- a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt +++ b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt @@ -2,8 +2,9 @@ package arrow import arrow.atomic.AtomicBoolean import io.kotest.assertions.AssertionErrorBuilder +import io.kotest.assertions.assertionCounter import io.kotest.common.reflection.bestName -import io.kotest.matchers.assertionCounter +import io.kotest.matchers.collections.shouldHaveSingleElement import io.kotest.matchers.shouldBe import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.Channel @@ -72,7 +73,7 @@ class AutoCloseTest { r.shutdown() throw error2 } - autoClose({ Resource() }) { _, _ -> throw error3 } + val _ = autoClose({ Resource() }) { _, _ -> throw error3 } require(wasActive.complete(r.isActive())) throw error } @@ -93,7 +94,7 @@ class AutoCloseTest { val e = shouldThrow { autoCloseScope { - autoClose({ Resource() }) { r, e -> + val _ = autoClose({ Resource() }) { r, e -> require(promise.complete(e)) r.shutdown() throw error2 @@ -163,7 +164,7 @@ class AutoCloseTest { val wasActive = Channel(Channel.UNLIMITED) val closed = Channel(Channel.UNLIMITED) - autoCloseScope { + val _ = autoCloseScope { val r1 = autoClose({ res1 }) { r, _ -> closed.trySend(r).getOrThrow() r.shutdown() @@ -190,28 +191,29 @@ class AutoCloseTest { closed.cancel() } + @Test + fun normalCancel() = shouldAutoCloseWithSecond({}) { throw CancellationException("second") } - @OptIn(ExperimentalStdlibApi::class) // 'AutoCloseable' in stdlib < 2.0 - private class Resource : AutoCloseable { - private val isActive = AtomicBoolean(true) + @Test + fun returnCancel(): Unit = shouldAutoCloseWithSecond({ return }) { throw CancellationException("second") } - fun isActive(): Boolean = isActive.get() + @Test + fun cancelCancel() = shouldAutoCloseWithSecond({ throw CancellationException("first") }) { throw CancellationException("second") } - fun shutdown() { - require(isActive.compareAndSet(expected = true, new = false)) { - "Already shut down" - } - } + @Test + fun throwCancel() = shouldAutoCloseWithFirst({ throw RuntimeException("first") }) { throw CancellationException("second") } - override fun close() { - shutdown() - } - } + @Test + fun normalThrow() = shouldAutoCloseWithSecond({}) { throw RuntimeException("second") } - private suspend fun CompletableDeferred.shouldHaveCompleted(): T { - isCompleted shouldBe true - return await() - } + @Test + fun returnThrow(): Unit = shouldAutoCloseWithSecond({ return }) { throw RuntimeException("second") } + + @Test + fun cancelThrow() = shouldAutoCloseWithSecond({ throw CancellationException("first") }) { throw RuntimeException("second") } + + @Test + fun throwThrow() = shouldAutoCloseWithFirst({ throw RuntimeException("first") }) { throw RuntimeException("second") } } // copied from Kotest so we can inline it @@ -219,7 +221,7 @@ inline fun shouldThrow(block: () -> Any?): T { assertionCounter.inc() val expectedExceptionClass = T::class val thrownThrowable = try { - block() + val _ = block() null // Can't throw failure here directly, as it would be caught by the catch clause, and it's an AssertionError, which is a special case } catch (thrown: Throwable) { thrown @@ -237,3 +239,71 @@ inline fun shouldThrow(block: () -> Any?): T { .build() } } + +private class Resource : AutoCloseable { + private val isActive = AtomicBoolean(true) + + fun isActive(): Boolean = isActive.get() + + fun shutdown() { + require(isActive.compareAndSet(expected = true, new = false)) { + "Already shut down" + } + } + + override fun close() { + shutdown() + } +} + +private suspend fun CompletableDeferred.shouldHaveCompleted(): T { + isCompleted shouldBe true + return await() +} + +private inline fun shouldAutoCloseWithFirst(first: () -> Unit, crossinline second: () -> Nothing) { + var firstThrowable: Throwable? = null + lateinit var secondThrowable: Throwable + try { + autoCloseScope { + onClose { + it shouldBe firstThrowable + peekThrowable(second) { secondThrowable = it } + } + peekThrowable(first) { firstThrowable = it } + } + } catch (e: Throwable) { + e shouldBe firstThrowable + e.suppressedExceptions shouldHaveSingleElement secondThrowable + } finally { + val _ = secondThrowable // ensure that onClose ran + } +} + +private inline fun shouldAutoCloseWithSecond(first: () -> Unit, crossinline second: () -> Nothing) { + var firstThrowable: Throwable? = null + lateinit var secondThrowable: Throwable + var finishedWithThrowable = false + try { + autoCloseScope { + onClose { + it shouldBe firstThrowable + peekThrowable(second) { secondThrowable = it } + } + peekThrowable(first) { firstThrowable = it } + } + } catch (e: Throwable) { + e shouldBe secondThrowable + if (firstThrowable != null) e.suppressedExceptions shouldHaveSingleElement firstThrowable + finishedWithThrowable = true + } finally { + finishedWithThrowable shouldBe true // otherwise, we finished with first somehow, either non-locally, or with Unit + } +} + +private inline fun peekThrowable(block: () -> R, peek: (Throwable) -> Unit): R = try { + block() +} catch (e: Throwable) { + peek(e) + throw e +} diff --git a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt index e6918deaf11..dfb72335169 100644 --- a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt +++ b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt @@ -12,9 +12,15 @@ public infix fun Throwable?.mergeSuppressed(other: Throwable?): Throwable? { returns(null) implies (this@mergeSuppressed == null && other == null) } return when { + // other completed normally other == null -> this + // this completed normally or with a non-local return this == null -> other + // this completed with a cancellation + this is CancellationException -> other.also { other.addSuppressed(this) } + // other completed with a cancellation other is CancellationException -> this.also { addSuppressed(other) } + // both completed exceptionally else -> this.also { addSuppressed(other.nonFatalOrThrow()) } } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index f13acc790cf..e992bac46e3 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -656,7 +656,7 @@ class ResourceTest { throw suppressed }.allocate() - val exception = shouldThrow { + val exception = shouldThrow { try { allocated shouldBe seed throw cancellation @@ -665,8 +665,8 @@ class ResourceTest { } } - exception shouldBe cancellation - exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe suppressed + exception shouldBe suppressed + exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe cancellation released.shouldHaveCompleted().shouldBeTypeOf() } } From a7da425f220d64883fd7d837bbcc05a8097b95ae Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 1 Jul 2026 21:15:10 +0100 Subject: [PATCH 2/7] Add ControlCancellationException and deprioritize it in mergeSuppressed --- .../commonTest/kotlin/arrow/AutoCloseTest.kt | 32 ++++++++++++++++--- .../core/raise/RaiseCancellationException.kt | 6 ++-- .../kotlin/arrow/core/raise/Fold.kt | 5 ++- .../core/raise/RaiseCancellationException.kt | 6 ++-- .../core/raise/RaiseCancellationException.kt | 6 ++-- .../core/ControlCancellationException.kt | 21 ++++++++++++ .../kotlin/arrow/core/ThrowableUtils.kt | 19 ++++++++--- 7 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt diff --git a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt index d4fff6478ba..be7f3b9a55b 100644 --- a/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt +++ b/arrow-libs/core/arrow-autoclose/src/commonTest/kotlin/arrow/AutoCloseTest.kt @@ -1,6 +1,8 @@ package arrow import arrow.atomic.AtomicBoolean +import arrow.core.ControlCancellationException +import arrow.core.InternalArrowApi import io.kotest.assertions.AssertionErrorBuilder import io.kotest.assertions.assertionCounter import io.kotest.common.reflection.bestName @@ -13,6 +15,7 @@ import kotlinx.coroutines.test.runTest import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test +@OptIn(InternalArrowApi::class) class AutoCloseTest { @Test @@ -191,14 +194,32 @@ class AutoCloseTest { closed.cancel() } + @Test + fun normalRaise() = shouldAutoCloseWithSecond({}) { throw ControlCancellationException("second") } + + @Test + fun returnRaise() = shouldAutoCloseWithSecond({ return }) { throw ControlCancellationException("second") } + + @Test + fun raiseRaise() = shouldAutoCloseWithSecond({ throw ControlCancellationException("first") }) { throw ControlCancellationException("second") } + + @Test + fun cancelRaise() = shouldAutoCloseWithFirst({ throw CancellationException("first") }) { throw ControlCancellationException("second") } + + @Test + fun throwRaise() = shouldAutoCloseWithFirst({ throw RuntimeException("first") }) { throw ControlCancellationException("second") } + @Test fun normalCancel() = shouldAutoCloseWithSecond({}) { throw CancellationException("second") } @Test - fun returnCancel(): Unit = shouldAutoCloseWithSecond({ return }) { throw CancellationException("second") } + fun returnCancel() = shouldAutoCloseWithSecond({ return }) { throw CancellationException("second") } @Test - fun cancelCancel() = shouldAutoCloseWithSecond({ throw CancellationException("first") }) { throw CancellationException("second") } + fun raiseCancel() = shouldAutoCloseWithSecond({ throw ControlCancellationException("first") }) { throw CancellationException("second") } + + @Test + fun cancelCancel() = shouldAutoCloseWithFirst({ throw CancellationException("first") }) { throw CancellationException("second") } @Test fun throwCancel() = shouldAutoCloseWithFirst({ throw RuntimeException("first") }) { throw CancellationException("second") } @@ -207,10 +228,13 @@ class AutoCloseTest { fun normalThrow() = shouldAutoCloseWithSecond({}) { throw RuntimeException("second") } @Test - fun returnThrow(): Unit = shouldAutoCloseWithSecond({ return }) { throw RuntimeException("second") } + fun returnThrow() = shouldAutoCloseWithSecond({ return }) { throw RuntimeException("second") } + + @Test + fun raiseThrow() = shouldAutoCloseWithSecond({ throw ControlCancellationException("first") }) { throw RuntimeException("second") } @Test - fun cancelThrow() = shouldAutoCloseWithSecond({ throw CancellationException("first") }) { throw RuntimeException("second") } + fun cancelThrow() = shouldAutoCloseWithFirst({ throw CancellationException("first") }) { throw RuntimeException("second") } @Test fun throwThrow() = shouldAutoCloseWithFirst({ throw RuntimeException("first") }) { throw RuntimeException("second") } diff --git a/arrow-libs/core/arrow-core/src/androidAndJvmMain/kotlin/arrow/core/raise/RaiseCancellationException.kt b/arrow-libs/core/arrow-core/src/androidAndJvmMain/kotlin/arrow/core/raise/RaiseCancellationException.kt index a5082ef1f62..d50982eb0b6 100644 --- a/arrow-libs/core/arrow-core/src/androidAndJvmMain/kotlin/arrow/core/raise/RaiseCancellationException.kt +++ b/arrow-libs/core/arrow-core/src/androidAndJvmMain/kotlin/arrow/core/raise/RaiseCancellationException.kt @@ -1,9 +1,11 @@ package arrow.core.raise -import kotlin.coroutines.cancellation.CancellationException +import arrow.core.ControlCancellationException +import arrow.core.InternalArrowApi +@OptIn(InternalArrowApi::class) @DelicateRaiseApi public actual sealed class RaiseCancellationException actual constructor( internal actual val raised: Any?, internal actual val raise: Raise -) : CancellationException(RaiseCancellationExceptionCaptured) +) : ControlCancellationException(RaiseCancellationExceptionCaptured) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt index 8c120211528..7762102a875 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Fold.kt @@ -6,7 +6,9 @@ package arrow.core.raise import arrow.atomic.AtomicBoolean +import arrow.core.ControlCancellationException import arrow.core.Either +import arrow.core.InternalArrowApi import arrow.core.nonFatalOrThrow import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE @@ -271,11 +273,12 @@ public annotation class DelicateRaiseApi * [RaiseCancellationException] is a _delicate_ api, and should be used with care. * It drives the short-circuiting behavior of [Raise]. */ +@OptIn(InternalArrowApi::class) @DelicateRaiseApi public expect sealed class RaiseCancellationException( raised: Any?, raise: Raise -) : CancellationException { +) : ControlCancellationException { internal val raised: Any? internal val raise: Raise } diff --git a/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt b/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt index e96ccd8ab28..ecb2fd74774 100644 --- a/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt +++ b/arrow-libs/core/arrow-core/src/jsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt @@ -1,7 +1,8 @@ package arrow.core.raise +import arrow.core.ControlCancellationException +import arrow.core.InternalArrowApi import kotlinx.js.JsPlainObject -import kotlin.coroutines.cancellation.CancellationException /** * There is no direct way to create an instance of [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) without a stack in JS. @@ -15,11 +16,12 @@ internal external interface RaiseCancellationExceptionLike { val raise: Raise } +@OptIn(InternalArrowApi::class) @DelicateRaiseApi public actual sealed class RaiseCancellationException actual constructor( raised: Any?, raise: Raise -) : CancellationException(RaiseCancellationExceptionCaptured) { +) : ControlCancellationException(RaiseCancellationExceptionCaptured) { private val _raised = raised private val _raise = raise diff --git a/arrow-libs/core/arrow-core/src/nonJvmAndJsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt b/arrow-libs/core/arrow-core/src/nonJvmAndJsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt index a5082ef1f62..d50982eb0b6 100644 --- a/arrow-libs/core/arrow-core/src/nonJvmAndJsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt +++ b/arrow-libs/core/arrow-core/src/nonJvmAndJsMain/kotlin/arrow/core/raise/RaiseCancellationException.kt @@ -1,9 +1,11 @@ package arrow.core.raise -import kotlin.coroutines.cancellation.CancellationException +import arrow.core.ControlCancellationException +import arrow.core.InternalArrowApi +@OptIn(InternalArrowApi::class) @DelicateRaiseApi public actual sealed class RaiseCancellationException actual constructor( internal actual val raised: Any?, internal actual val raise: Raise -) : CancellationException(RaiseCancellationExceptionCaptured) +) : ControlCancellationException(RaiseCancellationExceptionCaptured) diff --git a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt new file mode 100644 index 00000000000..51e3250eabb --- /dev/null +++ b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt @@ -0,0 +1,21 @@ +package arrow.core + +import kotlin.coroutines.cancellation.CancellationException + +@MustBeDocumented +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn("This declaration is public only to allow other arrow libraries to use it", RequiresOptIn.Level.ERROR) +public annotation class InternalArrowApi + +/** + * [ControlCancellationException] is a _delicate_ api, and should be used with care. + * It denotes a short-circuiting exception. + * Exceptions of this type are deprioritized w.r.t. exception suppression. + * + * @see mergeSuppressed + */ +@InternalArrowApi +public open class ControlCancellationException: CancellationException { + public constructor() + public constructor(message: String?) +} diff --git a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt index dfb72335169..e11afaae3be 100644 --- a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt +++ b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ThrowableUtils.kt @@ -6,7 +6,16 @@ import kotlin.coroutines.cancellation.CancellationException public fun Throwable?.throwIfNotNull() { if (this != null) throw this } -@OptIn(ExperimentalContracts::class) +/** + * Merges two nullable [Throwable] values by adding one as suppressed to the other. + * + * Returns the non-null throwable when only one is present, or `null` when both are `null`. + * [ControlCancellationException]s are deprioritized in the presence of other exceptions. + * + * @param other Another throwable to merge with this one. + * @return The merged throwable, or `null` if both are `null`. + */ +@OptIn(ExperimentalContracts::class, InternalArrowApi::class) public infix fun Throwable?.mergeSuppressed(other: Throwable?): Throwable? { contract { returns(null) implies (this@mergeSuppressed == null && other == null) @@ -16,11 +25,11 @@ public infix fun Throwable?.mergeSuppressed(other: Throwable?): Throwable? { other == null -> this // this completed normally or with a non-local return this == null -> other - // this completed with a cancellation - this is CancellationException -> other.also { other.addSuppressed(this) } - // other completed with a cancellation + // this completed with raise + this is ControlCancellationException -> other.also { other.addSuppressed(this) } + // other completed with cancellation or raise other is CancellationException -> this.also { addSuppressed(other) } - // both completed exceptionally + // both completed exceptionally or this completed with cancellation else -> this.also { addSuppressed(other.nonFatalOrThrow()) } } } From dc92530db902a2eb809d3b78540008d1af2e357c Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 1 Jul 2026 21:30:52 +0100 Subject: [PATCH 3/7] Fix #3925 --- .../src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt index 6e8073bc675..28f5f18f60e 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Race3.kt @@ -4,7 +4,7 @@ package arrow.fx.coroutines import arrow.core.mergeSuppressed -import arrow.core.nonFatalOrThrow +import arrow.core.throwIfFatal import arrow.core.throwIfNotNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -20,6 +20,7 @@ import kotlin.contracts.contract import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.cancellation.CancellationException public sealed class Race3 { public data class First(val winner: A) : Race3() @@ -114,13 +115,13 @@ internal suspend fun cancelAndCompose(first: Deferred<*>, second: Deferred<*>) { first.cancelAndJoin() null } catch (e: Throwable) { - e.nonFatalOrThrow() + e.also { if (e !is CancellationException) e.throwIfFatal() } } val e2 = try { second.cancelAndJoin() null } catch (e: Throwable) { - e.nonFatalOrThrow() + e.also { if (e !is CancellationException) e.throwIfFatal() } } (e1 mergeSuppressed e2).throwIfNotNull() } From e7dc5230a7b70cfd2b2faa00b7093c719ae6c7ab Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 1 Jul 2026 21:31:19 +0100 Subject: [PATCH 4/7] Fix #3926 --- .../src/commonMain/kotlin/arrow/resilience/Saga.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt index 23e84a8d42e..920f7ff2ba8 100644 --- a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt +++ b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt @@ -157,7 +157,7 @@ internal class SagaBuilder( finalizer() acc } catch (e: Throwable) { - acc mergeSuppressed e.nonFatalOrThrow() + acc mergeSuppressed e } }.throwIfNotNull() } From cee6428337c77b65c57d0d747a844cb11a658d7c Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 1 Jul 2026 23:00:40 +0100 Subject: [PATCH 5/7] Pass message correctly in ControlCancellationException --- .../kotlin/arrow/core/ControlCancellationException.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt index 51e3250eabb..d217bc60511 100644 --- a/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt +++ b/arrow-libs/core/arrow-exception-utils/src/commonMain/kotlin/arrow/core/ControlCancellationException.kt @@ -15,7 +15,7 @@ public annotation class InternalArrowApi * @see mergeSuppressed */ @InternalArrowApi -public open class ControlCancellationException: CancellationException { - public constructor() - public constructor(message: String?) +public open class ControlCancellationException : CancellationException { + public constructor() : super() + public constructor(message: String?) : super(message) } From 45f8f304161c7519216f6176d0c46157751f434a Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Wed, 1 Jul 2026 23:30:21 +0100 Subject: [PATCH 6/7] Undo ResourceTest behavior change --- .../arrow/fx/coroutines/ResourceTest.kt | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index e992bac46e3..a4c6ca983cd 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -1,7 +1,9 @@ package arrow.fx.coroutines import arrow.atomic.AtomicBoolean +import arrow.core.ControlCancellationException import arrow.core.Either +import arrow.core.InternalArrowApi import arrow.core.getOrElse import arrow.core.left import arrow.core.none @@ -656,6 +658,36 @@ class ResourceTest { throw suppressed }.allocate() + val exception = shouldThrow { + try { + allocated shouldBe seed + throw cancellation + } catch (e: Throwable) { + release(ExitCase(e)) + } + } + + exception shouldBe cancellation + exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe suppressed + released.shouldHaveCompleted().shouldBeTypeOf() + } + } + + @OptIn(DelicateCoroutinesApi::class, InternalArrowApi::class) + @Test + fun allocateControlCancellationException() = runTest { + checkAll( + Arb.int(), + Arb.string().map { ControlCancellationException(it) }, + Arb.string().map(::IllegalStateException) + ) { seed, cancellation, thrown -> + val released = CompletableDeferred() + val (allocated, release) = + resource({ seed }) { _, exitCase -> + require(released.complete(exitCase)) + throw thrown + }.allocate() + val exception = shouldThrow { try { allocated shouldBe seed @@ -665,7 +697,7 @@ class ResourceTest { } } - exception shouldBe suppressed + exception shouldBe thrown exception.suppressedExceptions.firstOrNull().shouldNotBeNull() shouldBe cancellation released.shouldHaveCompleted().shouldBeTypeOf() } From 85fbddb847088d8f3682ac87e9b0c4f9032e3ac7 Mon Sep 17 00:00:00 2001 From: kyay10 <12699034+kyay10@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:03:52 +0000 Subject: [PATCH 7/7] Auto-update API files --- arrow-libs/core/arrow-core/api/arrow-core.klib.api | 2 +- arrow-libs/core/arrow-core/api/jvm/arrow-core.api | 2 +- .../api/arrow-exception-utils.klib.api | 9 +++++++++ .../api/jvm/arrow-exception-utils.api | 8 ++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/arrow-libs/core/arrow-core/api/arrow-core.klib.api b/arrow-libs/core/arrow-core/api/arrow-core.klib.api index 40318b916cb..d61c2e96ee0 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.klib.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.klib.api @@ -699,7 +699,7 @@ sealed class <#A: out kotlin/Any?> arrow.core/Option { // arrow.core/Option|null } } -sealed class arrow.core.raise/RaiseCancellationException : kotlin.coroutines.cancellation/CancellationException // arrow.core.raise/RaiseCancellationException|null[0] +sealed class arrow.core.raise/RaiseCancellationException : arrow.core/ControlCancellationException // arrow.core.raise/RaiseCancellationException|null[0] final object arrow.core/ArrowCoreInternalException : kotlin/RuntimeException // arrow.core/ArrowCoreInternalException|null[0] diff --git a/arrow-libs/core/arrow-core/api/jvm/arrow-core.api b/arrow-libs/core/arrow-core/api/jvm/arrow-core.api index d71d161485d..a51b8dae8de 100644 --- a/arrow-libs/core/arrow-core/api/jvm/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/jvm/arrow-core.api @@ -1024,7 +1024,7 @@ public abstract class arrow/core/raise/RaiseAccumulate$Value { public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Ljava/lang/Object; } -public abstract class arrow/core/raise/RaiseCancellationException : java/util/concurrent/CancellationException { +public abstract class arrow/core/raise/RaiseCancellationException : arrow/core/ControlCancellationException { public synthetic fun (Ljava/lang/Object;Larrow/core/raise/Raise;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } diff --git a/arrow-libs/core/arrow-exception-utils/api/arrow-exception-utils.klib.api b/arrow-libs/core/arrow-exception-utils/api/arrow-exception-utils.klib.api index 8d53f8eaeed..f572c30ab77 100644 --- a/arrow-libs/core/arrow-exception-utils/api/arrow-exception-utils.klib.api +++ b/arrow-libs/core/arrow-exception-utils/api/arrow-exception-utils.klib.api @@ -6,6 +6,15 @@ // - Show declarations: true // Library unique name: +open annotation class arrow.core/InternalArrowApi : kotlin/Annotation { // arrow.core/InternalArrowApi|null[0] + constructor () // arrow.core/InternalArrowApi.|(){}[0] +} + +open class arrow.core/ControlCancellationException : kotlin.coroutines.cancellation/CancellationException { // arrow.core/ControlCancellationException|null[0] + constructor () // arrow.core/ControlCancellationException.|(){}[0] + constructor (kotlin/String?) // arrow.core/ControlCancellationException.|(kotlin.String?){}[0] +} + final fun (kotlin/Throwable).arrow.core/nonFatalOrThrow(): kotlin/Throwable // arrow.core/nonFatalOrThrow|nonFatalOrThrow@kotlin.Throwable(){}[0] final fun (kotlin/Throwable?).arrow.core/mergeSuppressed(kotlin/Throwable?): kotlin/Throwable? // arrow.core/mergeSuppressed|mergeSuppressed@kotlin.Throwable?(kotlin.Throwable?){}[0] final fun (kotlin/Throwable?).arrow.core/throwIfNotNull() // arrow.core/throwIfNotNull|throwIfNotNull@kotlin.Throwable?(){}[0] diff --git a/arrow-libs/core/arrow-exception-utils/api/jvm/arrow-exception-utils.api b/arrow-libs/core/arrow-exception-utils/api/jvm/arrow-exception-utils.api index 7108f8bbefe..dac6a1a65a9 100644 --- a/arrow-libs/core/arrow-exception-utils/api/jvm/arrow-exception-utils.api +++ b/arrow-libs/core/arrow-exception-utils/api/jvm/arrow-exception-utils.api @@ -1,3 +1,11 @@ +public class arrow/core/ControlCancellationException : java/util/concurrent/CancellationException { + public fun ()V + public fun (Ljava/lang/String;)V +} + +public abstract interface annotation class arrow/core/InternalArrowApi : java/lang/annotation/Annotation { +} + public final class arrow/core/NonFatalKt { public static final fun NonFatal (Ljava/lang/Throwable;)Z }