Rob Elliot
09/23/2025, 11:17 AMsealed interface FooResult
data object FooCancelled : FooResult
data class FooSuccess : FooResult
suspend fun foo(): FooResult = TODO()
val deferred = async { foo() }
deferred.cancel()
val result = deferred.await()
so that the last call will never throw CancellationException, instead returning FooCancelled? Without a try / catch (or equivalent) around deferred.await()?Youssef Shoaib [MOD]
09/23/2025, 12:57 PMlaunch, and simply cancel the launched coroutine and complete with FooCancelledRob Elliot
09/23/2025, 2:48 PMJobCancellationException on the call to deferred.await().
Perhaps it's just the nature of the beast, and I should stop fretting about it... I do hate exceptions though.
My attempt:
sealed interface FooResult
data object FooCancelled : FooResult
data class FooSuccess(val data: String) : FooResult
suspend fun doFoo(): FooResult {
delay(2000.milliseconds)
return FooSuccess("foo")
}
suspend fun foo(): Deferred<FooResult> = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
val result = CompletableDeferred<FooResult>()
try {
launch(result) {
val work = doFoo()
result.complete(work)
}
} catch (_: CancellationException) {
result.complete(FooCancelled)
}
result
}
fun main() {
val result = runBlocking {
val deferred = foo()
deferred.cancel()
deferred.await()
}
assert(result == FooCancelled) { "$result should be $FooCancelled" }
}Youssef Shoaib [MOD]
09/23/2025, 2:49 PMlaunch, and make sure to rethrow the exceptionRob Elliot
09/23/2025, 2:57 PMUncancellableDeferred did. Is this bad practice?
sealed interface FooResult
data object FooCancelled : FooResult
data class FooSuccess(val data: String) : FooResult
suspend fun foo(): Deferred<FooResult> = coroutineScope {
val result = CompletableDeferred<FooResult>()
launch(result) {
val work = doFoo()
result.complete(work)
}
UncancellableDeferred(FooCancelled, result)
}
private suspend fun doFoo(): FooResult {
delay(20000.milliseconds)
return FooSuccess("foo")
}
fun main() {
val result = runBlocking {
val deferred = foo()
deferred.cancel()
deferred.await()
}
assert(result == FooCancelled) { "$result should be $FooCancelled" }
}
@OptIn(InternalForInheritanceCoroutinesApi::class)
class UncancellableDeferred<T>(
private val ifCancelled: T,
private val delegate: Deferred<T>,
) : Deferred<T> by delegate {
override suspend fun await(): T = try {
delegate.await()
} catch (_: CancellationException) {
ifCancelled
}
}Rob Elliot
09/23/2025, 2:58 PM@OptIn(InternalForInheritanceCoroutinesApi::class) literally tells me that 😳Youssef Shoaib [MOD]
09/23/2025, 2:59 PMRob Elliot
09/23/2025, 3:10 PMRob Elliot
09/23/2025, 3:13 PMsealed interface FooResult
data object FooCancelled : FooResult
data class FooSuccess(val data: String) : FooResult
suspend fun foo(): SafeDeferred<FooResult> = coroutineScope {
val result = CompletableDeferred<FooResult>()
launch(result) {
val work = doFoo()
result.complete(work)
}
SafeDeferred(FooCancelled, result)
}
private suspend fun doFoo(): FooResult {
delay(3000.milliseconds)
return FooSuccess("foo")
}
fun main() {
val result = runBlocking {
val deferred = foo()
deferred.cancel()
deferred.await()
}
assert(result == FooCancelled) { "$result should be $FooCancelled" }
}
class SafeDeferred<T>(
private val ifCancelled: T,
private val delegate: Deferred<T>,
) {
suspend fun await(): T = try {
delegate.await()
} catch (_: CancellationException) {
ifCancelled
}
fun cancel(): Unit = delegate.cancel()
}
However, if I try and rewrite suspend fun foo() as so:
suspend fun foo(): SafeDeferred<FooResult> = coroutineScope {
val deferred = async { doFoo() }
SafeDeferred(FooCancelled, deferred)
}
the cancellation doesn't get propagated to delay, so I do get FooCancelled back but only after waiting 3 seconds. So clearly I'm still not getting how cancellation is propagated.Youssef Shoaib [MOD]
09/23/2025, 3:14 PMcoroutineScope waits for its children to complete, so I don't think that's what you want here...Zach Klippenstein (he/him) [MOD]
09/23/2025, 3:33 PMRob Elliot
09/23/2025, 3:44 PMRob Elliot
09/23/2025, 3:44 PMZach Klippenstein (he/him) [MOD]
09/23/2025, 4:27 PMRob Elliot
09/23/2025, 4:37 PMYoussef Shoaib [MOD]
09/24/2025, 4:34 AMfun CoroutineScope.foo() = safeDeferred(FooCancelled, async { doFoo() })
And that should do the trick. Maybe throw a simple ensureActive() in there so that you don't accidentally swallow a cancellationlouiscad
09/28/2025, 10:38 PM