https://kotlinlang.org logo
#coroutines
Title
# coroutines
k

kevinherron

12/14/2018, 12:28 PM
i've simplified, can anyone explain why this leads to an uncaught exception logged to console?
Copy code
fun main(args: Array<String>) {
    val job = SupervisorJob()
    val scope = CoroutineScope(job + Dispatchers.Default)

    val deferred = GlobalScope.async(Dispatchers.Default, start = CoroutineStart.LAZY) {
        delay(5000)
        throw Exception("boom")
    }

    scope.launch {
        try {
            deferred.await()
        } catch (e: Exception) {
            println("error awaiting: $e")
        }
    }

    runBlocking { job.cancelAndJoin() }

    Thread.sleep(10000)
}
e

Evgeniy Zaharov

12/14/2018, 12:34 PM
you start
async
in another scope (global) so it runs independent from your supervisor job (which exception you see like Exception in thread “DefaultDispatcher-worker-1”.. ) all other coroutines are in one scope, so then you cancel
job
by
cancelAndJoin
, you also cancel
launch
(running in scope.launch)
k

kevinherron

12/14/2018, 12:35 PM
sure, but why does that mean the exception goes to console now?
deferred is supposed to capture the exception in
async
e

Evgeniy Zaharov

12/14/2018, 12:36 PM
default exception handler for coroutine, i think
k

kevinherron

12/14/2018, 12:36 PM
if you comment out the cancellation you see the normal behavior where default exception handler is not invoked for
async
blocks
e

Evgeniy Zaharov

12/14/2018, 12:37 PM
sure, because exception will be handled by another coroutine in try-catch, but with cancel this coroutine couldn’t do it
k

kevinherron

12/14/2018, 12:39 PM
it's propagated the caller of
await()
. If I cancel the only caller, nothing is `await()`ing any more, so why the exception?
e

Evgeniy Zaharov

12/14/2018, 12:40 PM
but async coroutine is not canceled and stay running
k

kevinherron

12/14/2018, 12:42 PM
yes, but that doesn't mean the exception should leak to console via uncaught handler
even if I put another call to deferred.await() after the cancellation to actually receive or catch the result of it I get the exception in the console and the thrown exception from the 2nd await()
e

Evgeniy Zaharov

12/14/2018, 12:51 PM
when job cancelled, all child coroutines canceled too, at this point scope.launch is on suspend point at await, after cancelation it starts at this suspend point and throws exception (JobCancellationException).. all above is all ok, right? async coroutine after above cancelation is not canceled, and running farther and after delay throws exception. coroutine helfself is state machine, and at exception it returnWithError. not very sure in the future, but as I understand it Exception handler catch exception and see that no coroutine is await result, so it simple throws it farther
k

kevinherron

12/14/2018, 12:55 PM
but like i said, i can add another that awaits the result
and it behaves the same
e

Evgeniy Zaharov

12/14/2018, 1:54 PM
found this:
Copy code
is CancelledContinuation -> {
                    /*
                     * If continuation was cancelled, then all further resumes must be
                     * ignored, because cancellation is asynchronous and may race with resume.
                     * Racy exception are reported so no exceptions are lost
                     *
                     * :todo: we should somehow remember the attempt to invoke resume and fail on the second attempt.
                     */
                    if (proposedUpdate is CompletedExceptionally) {
                        handleException(proposedUpdate.cause)
                    }
                    return
                }
kotlinx.coroutines.AbstractContinuation:206.kt
and then comes to: kotlinx.coroutines.CoroutineExceptionHandlerImplKt:
Copy code
internal fun handleExceptionViaHandler(context: CoroutineContext, exception: Throwable) {
    // Invoke exception handler from the context if present
    try {
        context[CoroutineExceptionHandler]?.let {
            it.handleException(context, exception)
            return
        }
    } catch (t: Throwable) {
        handleCoroutineExceptionImpl(context, handlerException(exception, t))
        return
    }

    // If handler is not present in the context or exception was thrown, fallback to the global handler
    handleCoroutineExceptionImpl(context, exception)
}
no handles is present, so it throws with handleCoroutineExceptionImpl
Copy code
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
    // use additional extension handlers
    for (handler in handlers) {
        try {
            handler.handleException(context, exception)
        } catch (t: Throwable) {
            // Use thread's handler if custom handler failed to handle exception
            val currentThread = Thread.currentThread()
            currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
        }
    }

    // use thread's handler
    val currentThread = Thread.currentThread()
    currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
e

Evgeniy Zaharov

12/17/2018, 7:52 AM
thanks for reporting!
3 Views