Cancellation of withContext from the original coroutine

withContext only checks for cancellation before it starts running its block. If the coroutine withContext is called from is cancelled after it has started, it will continue suspending until it is finished.

I would like to execute code with a different context yet still be able to cancel it, while it is running, from the original coroutine.

I wrote a function for this:

suspend inline fun <T> withContextCancellable(context: CoroutineContext,
                                              crossinline block: suspend CoroutineScope.() -> T): T {
    val listener = coroutineContext[Job]?.invokeOnCompletion(onCancelling = true) { context.cancel() }

    return try {
        withContext(context) { block() }
    } finally {
        listener?.dispose()
        coroutineContext[Job]?.let {
            if (it.isCancelled) context.cancel()
        }
    }
}

However, I’m not too comfortable with using invokeOnCompletion to accomplish this. While this API seems to be quite stable, it is nevertheless internal.

Is there a better/already implemented way to accomplish this?

This code appears to artificially make the “different context” Job a child of the “original coroutine” Job but only while some block is running. I have a hard time seeing a use case for this. Is it really exactly what you want?

I kinda doubt it, but if this really is what you want, then you are working outside of structured concurrency so, yeah, you have to set up and manage relationships manually.

I’m guessing/hoping that you really want an actual parent/child Job relationship, though.

If the “different context” job is only for the withContext block and is intended for cancelling the withContext block early, then just defining it as a child of the “original coroutine” will work (val childJob = Job(parentJob). If this is the case, though, I’d recommend using async which creates the child job for you (Deferred extend Job) instead of withContext just since I think it’d be clearer and more idiomatic.

If your “different context” Job and “original coroutine” Job are each used for managing groups of coroutines, and, in general, want “different context” Job to cancel when “original coroutine” is cancelled or fails, then also just define “different context” Job as a child of “original coroutine” Job. Any code using a child Job (even a withContext block) will cancel when the parent Job is cancelled or fails.

If you just want the withContext code block to cancel when “original coroutine” cancels don’t need to cancel it otherwise, (your goal is just to switch Dispatchers or something), then don’t pass a context with a Job into withContext at all, just use the part of the context (like Dispatcher) that you care about. In that case the withContext block will run as part of an internally created child Job so it will work as desired.

Do any of these situations actually match your needs? If not, then I’m curious, what is your use case for needing withContextCancellable?

Thanks for your suggestions. I really don’t know why I tried to maintain this architecture, it doesn’t make too much sense. I suppose it’s best to look for a better solution before starting to work on one :sweat_smile:

I think what you may need here is a check on the isActive property of the coroutine scope in your withContext block, as the coroutine scope should be the receiver of the block. I heard about this from the coroutines guide (“Cancellation is Cooperative”). From the isActive property documentation:

Returns true when the current Job is still active (has not completed and was not cancelled yet).

Check this property in long-running computation loops to support cancellation.

Alternatively, calling yield() will cause your computation to suspend at a given point, allowing other coroutines to run on the same thread. If the coroutine is cancelled in the meantime, yield() will throw CancellationException and stop your code in the withContext() block.