Reputation: 59144
I want to start an async coroutine with a suspending function in a given parent CoroutineScope
, to produce a Deferred
that might then be used from any coroutine in that scope.
I would like its job to be cancelled if the parent's job is cancelled, but if the suspending function throws an exception, I need that captured in the resulting Deferred
without cancelling sibling jobs in the parent scope.
The way I'm doing it works fine, but I'm wondering if there's a simpler, more idomatic way than this:
fun <T> callIt(scope: CoroutineScope, block: suspend () -> T) : Deferred<T> {
val result = CompletableDeferred<T>()
scope.launch {
try {
result.complete(block())
} catch (e: Throwable) {
result.completeExceptionally(e)
}
}
return result
}
I like that the handling of exceptions from the suspending block
is obviously what I want, but I'm not too happy about building an async
out of launch
Things that don't work:
async
catches its exceptions, but the job still fails and cancels its parent. As @Rene commented: The documentation of async
says: " it cancels the parent job (or outer scope) on failure to enforce structured concurrency paradigm.".Upvotes: 5
Views: 741
Reputation: 6148
You can do it without a CompletableDeferred
. You need to create an SupervisorJob
and let the job from the CoroutineScope
be the parent of the new job. If the scope is cancelled the async
-coroutine is cancelled as well. But no exception inside the async
-coroutine will cancel the parent scope.
Because of an open issue https://github.com/Kotlin/kotlinx.coroutines/issues/1578 we need to explicitly complete the SupervisorJob.
fun <T> callIt(scope: CoroutineScope, block: suspend () -> T): Deferred<T> {
val supervisorJob = SupervisorJob(scope.coroutineContext[Job])
val deferred = scope.async(supervisorJob) {
block()
}
deferred.invokeOnCompletion {
supervisorJob.complete()
}
return deferred
}
Upvotes: 3