Why is `launch` allowed to cancel itself but `coro...
# coroutines
s
Why is
launch
allowed to cancel itself but
coroutineScope
isn’t?
This completes normally:
Copy code
coroutineScope {
    launch { cancel() }
}
but this fails:
Copy code
coroutineScope {
    cancel()
}
I can sort of see why they’d be different but I’d like to understand the underlying mechanics that make them so. As far as I understand it, both
launch
and
coroutineScope
create a new
Job
— so why do those jobs behave differently from one another?
k
That’s interesting question, so decided to check it myself. So it really depends how you run your
main
function - is it with
runBlocking
/
coroutineScope
/
supervisorScope
? If so, that’s why your whole program ends with exception (which is cancellation).
runBlocking
/
coroutineScope
/
supervisorScope
instead of cancelling the parent and children, would propagate the error up (re-throw). If those are top - level coroutines, then the error crashes the app.
Look here: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/JobSupport.kt#L342 if
isScopedCoroutine
is true (which is true for coroutineScope and supervisorScope) the error is propagated to parent.
s
Thanks for the link, that explains a lot 👍. It makes sense for cancellations coming from outside, but I’m wondering if it should behave differently when the job cancels itself. The current behaviour feels very strange to me. For example:
Copy code
println("Outer job is active: $isActive")
try {
    coroutineScope {
        cancel()
        println("Inner scope is active: $isActive")
    }
} finally {
    println("Outer job is active: $isActive")
}
When I run this, I get:
Copy code
Outer job is active: true
Inner scope is active: false
Outer job is active: true
Exception in thread "main" kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@612fc6eb
In other words, the inner scope is propagating a cancellation to the outer job, even though the outer job wasn’t cancelled. Surely that’s not supposed to happen?
k
what about
isCancelled
on outter job?
s
Copy code
println("Outer job is active: ${coroutineContext.job.isActive}")
println("Outer job is cancelled: ${coroutineContext.job.isCancelled}")
try {
    coroutineScope {
        cancel()
        println("Inner scope is active: ${coroutineContext.job.isActive}")
        println("Inner scope is cancelled: ${coroutineContext.job.isCancelled}")
    }
} finally {
    println("Outer job is active: ${coroutineContext.job.isActive}")
    println("Outer job is cancelled: ${coroutineContext.job.isCancelled}")
}
produces
Copy code
Outer job is active: true
Outer job is cancelled: false
Inner scope is active: false
Inner scope is cancelled: true
Outer job is active: true
Outer job is cancelled: false
Exception in thread "main" kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@491cc5c9
k
what’s your outer scope then? I think it does make sense, since scoped coroutine builders are re-throwing exception (basically, asking parent to handle it) and if there’s no parent, the thread exception handler is reached
but why it’s not marked as cancelled? Yet to find out 😄
s
In this case the outer job was just created with
runBlocking
, but I think the behaviour will be the same no matter how I run it. The cancellation exception always propagates outside the
coroutineScope
even though the parent job is not cancelled.
k
well try running it on scope:
Copy code
val scope = CoroutineScope(...)

scope.launch {}
inside launch, do coroutineScope with cancellation. You’ll notice that the program would exit without exception (because
scope
does not re-throws). Remember to add some delay below
launch
as runBlocking is not aware of that coroutine launched of
scope
s
In that case I expect that the exception will still (incorrectly?) propagate out of the
coroutineScope
, but won’t propagate outside of the
launch
, for the same reasons that we already found. Let me see if I can make a simple example to show it.
Copy code
scope.launch {
    try {
        coroutineScope {
            cancel()
            ensureActive()
        }
    } catch (e: CancellationException) {
        println("Caught a cancellation exception")
        println("Cancelled: ${coroutineContext.job.isCancelled}")
    }
}
produces
Copy code
Caught a cancellation exception
Cancelled: false
whereas
Copy code
scope.launch {
    try {
        cancel()
        ensureActive()
    } catch (e: CancellationException) {
        println("Caught a cancellation exception")
        println("Cancelled: ${coroutineContext.job.isCancelled}")
    }
}
produces
Copy code
Caught a cancellation exception
Cancelled: true
Both examples complete normally, without an exception, presumably because
launch
never propagates
CancellationException
to its parent as you showed in the code you linked to.
(I used
join()
to wait for the job to complete before exiting)
k
From your first example (
coroutineScope
) you catch block says that
cancelled: false
because you caught the cancellation yourself (because coroutine scope would re-throw exceptions), so it never reached the parent
on second example, calling
cancel
triggers this chained parent cancellation through
Job
instance (calling cancelParent). Your
catch
block is triggered, by calling
ensureActive
which throws CancellationException if coroutine is no longer active
Copy code
This method is a drop-in replacement for the following code, but with more precise exception:

if (!isActive) {
    throw CancellationException()
}
s
I guess my argument is that
coroutineScope
should not throw a
CancellationException
if the coroutine that invoked it has not been cancelled. It seems inconsistent with the way cancellation normally works.
Having said that, I might have just figured out the reason.
coroutineScope
can return a value, so it has to throw if there is no value to return 💡
Similar to
async { cancel() }.await()
in effect
Seems kind of obvious when I look at it that way 🤦
k
Yes, exactly 😊 otherwise the error would be swallowed by scoped coroutine (even the cancellation)
376 Views