Skip to content

Add cooperativeCatch as coroutine-safe alternative to runCatching #4059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ public final class kotlinx/coroutines/CompletionHandlerException : java/lang/Run
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
}

public final class kotlinx/coroutines/CooperativeCatchKt {
public static final fun cooperativeCatch (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun cooperativeMap (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
}

public abstract interface class kotlinx/coroutines/CopyableThreadContextElement : kotlinx/coroutines/ThreadContextElement {
public abstract fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement;
public abstract fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext;
Expand Down
34 changes: 34 additions & 0 deletions kotlinx-coroutines-core/common/src/CooperativeCatch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kotlinx.coroutines

/**
* Calls the specified function [block] and returns its encapsulated result if invocation was successful.
* If a [CancellationException] occurs during execution of the specified function [block], then it will
* be immediately thrown. Otherwise, any other [Throwable] exception that was thrown from the [block]
* function execution and encapsulating it as a failure.
*/
public inline fun <T, R> T.cooperativeCatch(block: T.() -> R): Result<R> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
}

/**
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value
* if this instance represents [success][Result.isSuccess]. If a [CancellationException] occurs during
* the execution of the given [transform] function, it will be thrown immediately. Otherwise, the
* original encapsulated [Throwable] exception if it is [failure][Result.isFailure].
*
* This function catches any [Throwable] exception thrown by [transform] function other than
* [CancellationException], and encapsulates it as a failure.
*
* See [map] for an alternative that rethrows all exceptions from `transform` function.
*/
public inline fun <R, T> Result<T>.cooperativeMap(transform: (value: T) -> R): Result<R> {
return getOrNull()?.let {
cooperativeCatch { transform(it) }
} ?: Result.failure(exceptionOrNull() ?: error("Unreachable state"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.Test
import kotlinx.coroutines.testing.*
import kotlin.test.*

@OptIn(ExperimentalCoroutinesApi::class)
internal class CooperativeCatchKtTest {

@Test
fun cooperativeCatchSuccess() = runTest {
val result = cooperativeCatch { 42 }

assertTrue(result.isSuccess)
assertEquals(42, result.getOrNull())
}

@Test
fun cooperativeCatchFailure() = runTest {
val result = cooperativeCatch {
error("exception thrown in cooperativeCatch")
42
}

assertTrue(result.isFailure)
assertNull(result.getOrNull())
}

@Test
fun cooperativeCatchPropagatesCancellationException() = runTest(UnconfinedTestDispatcher()) {
var cancellationExceptionWasPropagated = false
val job = backgroundScope.launch {
try {
cooperativeCatch {
while (true) {
ensureActive()
delay(100)
}
42
}
} catch (e: CancellationException) {
cancellationExceptionWasPropagated = true
throw e
}
}

job.cancel()

assertTrue(cancellationExceptionWasPropagated)
}

@Test
fun cooperativeMapAppliesTransform() = runTest {
val result = cooperativeCatch {
42
}.cooperativeMap {
"The answer to life, the universe, and everything? $it"
}

assertEquals("The answer to life, the universe, and everything? 42", result.getOrNull())
}

@Test
fun cooperativeMapEncapsulatesThrowables() = runTest {
val result = cooperativeCatch {
42
}.cooperativeMap {
error("exception in cooperativeMap")
"The answer to life, the universe, and everything? $it"
}

assertTrue(result.isFailure)
assertNull(result.getOrNull())
}

@Test
fun cooperativeMapPropagatesCancellationException() = runTest(UnconfinedTestDispatcher()) {
var cancellationExceptionWasPropagated = false
val job = backgroundScope.launch {
try {
cooperativeCatch {
42
}.cooperativeMap {
while (true) {
ensureActive()
delay(100)
}
}
} catch (e: CancellationException) {
cancellationExceptionWasPropagated = true
throw e
}
}

job.cancel()

assertTrue(cancellationExceptionWasPropagated)
}
}