Can someone please explain the result of this smal...
# coroutines
s
Can someone please explain the result of this small snippet?
Copy code
fun main() {
  val job = GlobalScope.launch(Dispatchers.Main) {
    println("This is executed before toBeCancelled")
    toBeCancelled()
    println("This is executed after toBeCancelled")
  }
}

suspend fun toBeCancelled() {
  withContext(EmptyCoroutineContext) {
    coroutineContext[Job]!!.cancel()
    println("This is executed before the delay")
    delay(2000)
    println("This is executed after the delay")
  }
}
Copy code
This is executed before toBeCancelled
This is executed before the delay
How come “This is executed after toBeCancelled” is not printed? My understanding is that
withContext
would create a child job, and calling
coroutineContext[Job]!!.cancel()
should only cancel that child job. How come it is impacting the parent?
s
Because calling
withContext { ... }
throws a
CancellationException
. This bubbles up to the caller.
Try this and you can see the exception being thrown:
Copy code
suspend fun toBeCancelled() {
    try {
      withContext(EmptyCoroutineContext) {
          coroutineContext[Job]!!.cancel()
          println("This is executed before the delay")
          delay(2000)
          println("This is executed after the delay")
      }
    } catch (e: Throwable) { println("withContext threw exception $e"); throw e }
}
Removing the
throw e
from the
catch
clause will eat that cancellation exception and “This is executed after toBeCancelled” will be printed out (note: Don’t eat/forget-to-rethrow CancellationExceptions. It can break the Structured Concurrency!)
s
Hmm. Is it a special case with
withContext
? The following doesn’t cancel the parent.
Copy code
fun main() {
  val job = GlobalScope.launch(Dispatchers.Main) {
    println("This is executed before toBeCancelled")
    launch {
      coroutineContext[Job]!!.cancel()
      println("This is executed before the delay")
      delay(2000)
      println("This is executed after the delay")
    }.join()
    println("This is executed after toBeCancelled")
  }
}
Is the parent/child relationship different here?
s
Because
launch { ... }.join()
is not like
withContex
withContext
is more like
async { .... }.await()
. It returns the value of its lambda.
s
I see
So when a result is expected, a cancellation bubbles up then.
s
A call to
launch
will never throw an exception. A call to
join
won’t either. A
launch
will cause the CoroutineScope’s CoroutineExceptionHandler to be called. a call to
async
will never throw an exception either, but a call to
await
will throw an exception (if there was one thrown by the lambda of
async
)
withContext
, like
async { ...}.await()
, re-throws any exception thrown in its lambda. And that exception can be a CancellationException (caused by a call to
cancel
somewhere)
You may be interested in the article I wrote here 🙂 : https://medium.com/the-kotlin-chronicle/coroutine-exceptions-3378f51a7d33
s
But in case of a non-cancellation exception within
launch
, that bubble up right?
Just tested it, indeed seems to.
Ok, that’s clearer in my head now. Thanks a lot. I’ll check your article.
s
I won’t bubble up outside of the launch. An exception from a
launch
will be handled by the CoroutineScope’s CoroutineExceptionHandler (CEH). If that exception is a CancellationException, nothing really will happen, except for the CoroutineScope getting cancelled (unless a SupervisorScope is used). If it is not a CancelationException, depending whether you have a CEH installed or not and the implementation of your CEH and the implementation of your app’s ‘uncaught-exception-handler’, your app may crash.
s
In the following case:
Copy code
fun main() {
  val job = GlobalScope.launch(Dispatchers.Main) {
    println("This is executed before toBeCancelled")
    try {
      launch {
        throw Exception("shiiiii")
      }.join()
    } catch (e: Exception) {
      println (e)
    }
    println("This is executed after toBeCancelled")
  }
}
I’m getting:
Copy code
This is executed before toBeCancelled
JobCancellationException: StandaloneCoroutine is cancelling; caused by Exception: shiiiii; job=StandaloneCoroutine{Cancelling}@11
This is executed after toBeCancelled
Exception: shiiiii
Seems like I’m able to catch it outside the launch?
p
I think in your initial code:
coroutineContext[Job]!!.cancel()
inside
withContext
will return the parents job.
withContext
switches Context but I don't think it creates a new child Job().
s
I printed the jobs and they were different.
p
humm, that defeats my statement. Never mind.