I am observing intriguing behavior with kotlinx.co...
# coroutines
l
I am observing intriguing behavior with kotlinx.coroutines 0.23.4 where I call
job.cancel(IllegalStateException(...))
from a callback, catch exceptions from the parent coroutine, but still get my app crashing. I replicated the issue in some tests, and the catch block is hit, but it seems the Exception is thrown no matter if it was caught in the parent coroutine. Is this really intended?
e
That is strange. Do you have a self-contained example with failing tests?
l
Not one that doesn't expose private code yet, but I'll try to make one right now
@elizarov Here is a simple reproducer:
Copy code
@Test
fun reproduceCancellationBug() = runBlocking {
    try {
        willCancel()
    } catch (e: Exception) {
        println("We caught something.. ;-)")
    }
}

private suspend fun willCancel() {
    val job = coroutineContext[Job]!!
    job.cancel(IllegalStateException("Illegal stuff, going to court."))
}
g
I saw the same behaviour (Android app). Unfortunately don’t have now self-contained example
e
@louiscad But this is not a bug… You cancel a Job, so Job is now cancelled, but you don’t invoke any cancellable suspending function, so no one there to throw an exception.
If you add
yield
after
willCancel
, then it is thrown and caught properly
l
@elizarov So
willCancel
is not cancellable itself? BTW, I edited the snippet on try.kotl.in to add
yield()
after
willCancel()
and the exception is still thrown
e
Not really. Suspending function is cancellable only if it invoked another cancellable suspending function (like
yield
,
delay
,
receive
, or
suspendCancellableCoroutine
at the lowest level)
Or if it explicitly checks for
isActive
l
@elizarov Adding a
yield()
after
willCancel()
helps having the
catch
block executed, but the exception is still not really caught, see the snippet on the link I shared
e
That is tricky. You cancelled
runBlocking
job with exception, so it is now doomed to fail with this exception. Catching it does not help.
l
How can I make the
willCancel
function cancel instead?
e
What are you trying to achive? If you want to terminate (cancel) current job, then you just throw an exception.
This way it can be caught.
You typically use
job.cancel(...)
to cancel somebody else’s job, not your own job.
l
I want to cancel it from a callback that is instantiated from the
willCancel
function
e
Then you do
suspendCancellableCoroutine { cont -> ... }
and then while it is suspended you can
cont.cancel()
(cancelling continuation is different — it will throw an exception that can be caught up the stack)
l
That seems tricky to adapt to my current code. The class where I'm using has only one
suspend fun
that was there for the sole purpose of retrieving a
Job
to cancel. Your solution won't be able to cancel the coroutine after it has returned
e
Hm… I’m lost in your use-case. So, you want to retrieve current job, install callback, but let coroutine run, and the cancel it some time later?
It looks like you should define your own coroutine builder that clearly delimits the scope of your callback. Otherwise, it is completely non-clear how long your callback is being installed and what job it will cancel.
l
The scope would be the coroutine who called this function. So it would be there from the time it's run to the completion of the coroutine. The use case it to calibrate an accelerometer, and while it's being done, check that device is kept still, screen facing sky, cancelling the calibration process otherwise. Sensors monitoring is callback based, so I do check on each new sensor event, and I'd like to abort if I see the device has moved (but it currently aborts the whole app)
e
That’s quite fragile. I would not recommend this design pattern. When I’m invoking some suspending function
foo
, the coroutine, that it is part of, could be anywhere up the stack. I’d prefer and explicitly scoped block that clearly demarcates the beginning and the end of the corresponding scope.
Look at
withTimeout
, for example. We don’t attach timeout to the current coroutine. Instead, we put all the code that need to have timeout in a separate scope.
l
That's right, it's a great example, I'll dig into the source to see how I can do something similar
Thanks for your help!
@elizarov Is it normal behavior that cancelling a job with a
CancellationException
(or leaving to default one) crashes the program though? I thought only non
CancellationException
did so.
e
It “crashes” only w.r.t.
runBlocking
. It has to throw
CancellationException
, but the
fun main
would still print it on the console. If a coroutine terminated with
CancellationException
it is considered “normal”
l
Isn't there any way to cancel a coroutine from the non suspending world. I tried to write a method similar to
withTimeout
, but it's forcing me to do code duplication as it needs to be used conditionally, which is not possible with this approach out of the box (while I could just swap listeners if I could cancel the coroutine from the callbacks)
e
You can pass a Job that you’ve created into a non-suspending world and cancel it from there.
l
I see. Thanks for the hint. I tried the
withXx
way still, with
suspendCancellableCoroutine
, which I like since it is a bit more readable, but I don't know if I should use
resume
and
resumeWithException
or
tryResume
and
tryResumeWithException
or
completeResume
and
cancel
. Also, when I run tests (Android instrumented tests), the coroutine launched with
runBlocking
never resumes when
cont.resume(...)
is invoked, but it works fine in non tests, where it's run with
launch
from the UI thread instead. Any clue on what may cause this?