Question regarding `kotlinx.coroutines.AwaitKt#awa...
# coroutines
i
Question regarding
kotlinx.coroutines.AwaitKt#awaitAll
. From KDocs:
Awaits for completion of given deferred values without blocking a thread and resumes normally with the list of values when all deferred computations are complete or resumes with the first thrown exception if any of computations complete exceptionally including cancellation.
If I understand that right
resumes with the first thrown exception
. But having this example:
Copy code
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("exception handler $exception")
    }
    val scope = CoroutineScope(Dispatchers.Default + exceptionHandler)
    scope.launch {
        val firstCoroutine = async {
            delay(300)
            println("crashing first coroutine")
            throw IllegalArgumentException()
        }

        val secondCoroutine = async {
            try {
                delay(700)
                println("second coroutine still alive")
            } catch (e: Exception) {
                println("second coroutine try/catch $e")
            }
            "Done"
        }

        try {
            awaitAll(firstCoroutine, secondCoroutine)
        } catch (e: Exception) {
            println("awaitAll try/catch $e")
        }
    }
I was expecting to see
awaitAll try/catch  java.lang.IllegalArgumentException
but instead I see
awaitAll try/catch kotlinx.coroutines.JobCancellationException: Job is cancelling; job=StandaloneCoroutine{Cancelling}@27e6316a
. Could someone pls explain this behaviour?
b
Because of structured concurrency
The first
async
completes exceptionally, which cancels the second
async
instead of waiting for it to complete
It’s hard to say which one it sees is cancelling and fails to await on
t
Copy code
scope.launch {
+        println("launch scope = $this")
it shows StandaloneCoroutine same as catched cancelled job. maybe throwed exception in async is redirected to parent job. you can change receiver of async{} to GlobalScope.async{} to prevent cancel redirection.
the implementation of async{} is DeferredCoroutine. it has
override val cancelsParent: Boolean get() = true
i
well the question is more about why it reports the
JobCancellationException
that obviously comes from the second coroutine
delay
? I was expecting it will report exception from first coroutine which is
IllegalArgumentException
and that fails first
t
firstCoroutine raises exception. that exception is kept for await(), then firstCoroutine job is cancelled. cancelled is redirected to parent launch{...}. suspend in awaitAll() detect cancelld in current scope ( launch{...} ).
i
but wait
t
if you want to continue launch{}'s scope, change the parent of async{}. like as GlovalScope.async{} or CoroutineScope(...).async{}
i
no this doesnt explain this then
Copy code
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("exception handler $exception")
    }
    val scope = CoroutineScope(Dispatchers.Default + exceptionHandler)
    scope.launch {
        val firstCoroutine = async {
            delay(300)
            println("crashing first coroutine")
            throw IllegalArgumentException()
        }

        val secondCoroutine = async {
            try {
                delay(700)
                println("second coroutine still alive")
            } catch (e: Exception) {
                println("second coroutine try/catch $e")
            }
            "Done"
        }
        try {
            firstCoroutine.await()
        } catch (e: Exception) {
            println("awaitAll try/catch $e")
        }
//        try {
//            awaitAll(firstCoroutine, secondCoroutine)
//        } catch (e: Exception) {
//            println("awaitAll try/catch $e")
//        }
    }
by following your logic then this should report the same
error JobCancellationException
but it reports correct error
t
If coroutine receive exception AFTER set cancelled state, its behavior becomes bit strange.
I don't know the detail of
awaitAll()
, but I know
async{}
redirects cancelled state to the parent job.
you can print
${e.cause}
to get IllegalArgumentException
hmm, it's documented behavior of awaitAll()
Copy code
* This suspending function is cancellable.
 * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
 * this function immediately resumes with [CancellationException].
b
Cancelled in documentation seems to usually refer to “normal cancellation”.
Documentation of
Job
says
Copy code
Normal cancellation of a job is distinguished from its failure by the type of its cancellation exception cause. If the cause of cancellation is CancellationException, then the job is considered to be cancelled normally. This usually happens when cancel is invoked without additional parameters. If the cause of cancellation is a different exception, then the job is considered to have failed. This usually happens when the code of the job encounters some problem and throws an exception.