This example, taken from <https://kt.academy/artic...
# coroutines
c
This example, taken from https://kt.academy/article/cc-exception-handling, throws an exception:
Copy code
fun main(): Unit = runBlocking {
    // DON'T DO THAT!
    launch(SupervisorJob()) { // 1
        launch {
            delay(1000)
            throw Error("Some error")
        }
    }

    delay(3000)
}
However, this isn't what I expected to happen. Since a CoroutineContext argument is specified, I would expect that the job would be used. Looking at the implementation of
launch
;
Copy code
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else    // ← HERE
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
Since the coroutine is started with a parent of
newContext
, which includes our job, I would expect that the launched coroutine is indeed a child of
SupervisorJob()
. However, since
SupervisorJob()
has no parent, I would expect the exception to be swallowed or logged one way or another, but the other code to continue executing, since there is no parent-child relationship between them. Thus, I would have expected the following code to be correct:
Copy code
fun main(): Unit = runBlocking {
    // DON'T DO THAT!
    launch(SupervisorJob(coroutineContext.job)) { // 1
        launch {
            delay(1000)
            throw Error("Some error")
        }
    }

    delay(3000)
}
since the
SupervisorJob
is created as a child of
runBlocking
, I would expect it to behave the same as
supervisorScope { launch {} }
. Why does it not behave that way, what did I misunderstand?
s
What makes you say that the first example throws an exception?
c
I ran it 😅
there's a playground link in the article
s
You might seen an error in the console, but it's coming from the thread's uncaught exception handler; it doesn't mean the program crashed
c
OH.
s
If you add some more code at the end of the program, you can see that it continues running even after the exception is logged
So actually, you hit the nail on the head with this:
I would expect the exception to be swallowed or logged one way or another, but the other code to continue executing, since there is no parent-child relationship between them.
The playground can be misleading because it tends to buffer output and show it all at once. It's easier to see if you run it locally that the error happens well before the program terminates.
c
Ah, I get what the article is saying now. The hierarchy, in my second article, is
Copy code
runBlocking
  SupervisorJob()
    launch
       …
So, if you start multiple jobs within the
launch
, they are in a regular job and will cancel each other. However, the error won't bubble up to
runBlocking
, right?
s
👍 exactly
Basically, don't use
SupervisorJob()
except when making a
CoroutineScope()
.
🙏 1
c
@marcinmoskala you may want to reword the article, it makes it sound like
launch
will ignore the
SupervisorJob
argument, which doesn't seem to be true; it will just create the child job that is a regular
Job
. The
SupervisorJob
will be part of the hierarchy. The real bug in the example is that
SupervisorJob()
should be replaced by
SupervisorJob(coroutineContext.job)
, otherwise it breaks structured concurrency.
s
Technically, yes, you can pass a custom job while retaining structured concurrency. But the result is always going to be confusing and error-prone, as you found with the cancellation. So personally, I'd always treat passing a
Job
to
launch
as a bug, regardless of whether it has a parent. But I do take your point; there are different degrees of brokenness at play.
c
Hm, what your thoughts on https://gitlab.com/opensavvy/groundwork/pedestal/-/blob/main/cache/src/commonMain/kotlin/MemoryCache.kt?ref_type=heads#L62-66? This class acts as a daemon that starts jobs internally. However, it always uses the caller's coroutine context (except the job that it overrides by its own to "adopt" the operation). It's not explicitly written as a
launch
with a
Job
argument, but it's basically the same thing, no?
s
That looks like a different kind of issue: a suspending function that launches a coroutine without waiting for it to finish.
c
(that's indeed what it does, but that's on purpose)
m
c
Yes
m
Indeed runBlocking is not affected by exception, it is because it is not related to launch. The true hierarchy is:
Copy code
runBlocking with no children

SupervisorJob
   launch
      launch
      launch
I will try to clarify that
👍 1
That is why this delay is needed, without it runBlocking would complete immedietely as it has no children