If async throws before await: The exception is lost and is NOT processed by CoroutineExceptionHandler

Code

package com.glassthought.sandbox
 
import com.glassthought.sandbox.util.out.impl.out
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
 
 
fun main(): Unit {
  val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
    runBlocking(CoroutineName("CoroutineExceptionHandler")) {
      out.error("Caught exception in CoroutineExceptionHandler: ${ex::class.simpleName} with message=[${ex.message}].")
    }
  }
 
  val mainJob = Job()
  val scope = CoroutineScope(mainJob + coroutineExceptionHandler)
 
  scope.launch(CoroutineName("SpawnedFromMain")) {
    out.actionWithMsg("foo", { foo(scope) })
  }
 
  runBlocking {
    out.actionWithMsg("mainJob.join()", { mainJob.join() })
  }
}
 
private suspend fun foo(scope: CoroutineScope) {
  val deferred = scope.async(CoroutineName("async-1")) {
    out.actionWithMsg("throw-exception", {
      throw MyRuntimeException.create("exception-from-async-before-it-got-to-await", out)
    }, delayDuration = 500.milliseconds)
  }
 
  out.info("Just launched co-routines.")
  out.actionWithMsg(
    "deferred.await()",
    {
      deferred.await()
    },
    delayDuration = 1.seconds
  )
}

Command to reproduce:

gt.sandbox.checkout.commit 0423eb04f6570cb822dd \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed:   19ms][①][coroutname:@SpawnedFromMain#1] [->] action=[foo] is starting.
[INFO][elapsed:   19ms][⓶][coroutname:@coroutine#2] [->] action=[mainJob.join()] is starting.
[INFO][elapsed:   32ms][①][coroutname:@SpawnedFromMain#1]    Just launched co-routines.
[INFO][elapsed:   36ms][①][coroutname:@SpawnedFromMain#1]    [🐢] action=[deferred.await()] is being delayed for=[1000 ms] before starting.
[INFO][elapsed:   36ms][⓷][coroutname:@async-1#3] [🐢] action=[throw-exception] is being delayed for=[500 ms] before starting.
[INFO][elapsed:  536ms][⓷][coroutname:@async-1#3] [->] action=[throw-exception] is starting.
[WARN][elapsed:  566ms][⓷][coroutname:@async-1#3]    💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async-before-it-got-to-await]
[WARN][elapsed:  567ms][⓷][coroutname:@async-1#3] [<-][💥] Finished action=[throw-exception], it THREW exception of type=[MyRuntimeException] we are rethrowing it.
[INFO][elapsed:  583ms][①][coroutname:@SpawnedFromMain#1] [<-][🫡] Cancellation Exception - rethrowing.
[INFO][elapsed:  583ms][⓶][coroutname:@coroutine#2] [<-] Finished action=[mainJob.join()].
GT-Sandbox-Snapshot: Example where awaiting co-routine got cancelled before getting to await

Code

package com.glassthought.sandbox
 
import kotlinx.coroutines.*
 
val startMillis = System.currentTimeMillis()
 
 
fun myPrint(msg: String) {
  val elapsed = System.currentTimeMillis() - startMillis
 
  println("[${elapsed.toString().padStart(3)} ms] $msg")
}
 
fun main() {
  myPrint("[MAIN] START...")
 
  val mainScopeJob = Job()
  val mainScope = CoroutineScope(
    mainScopeJob
      + Dispatchers.IO
      + CoroutineExceptionHandler { _, throwable -> myPrint("👍 CoroutineExceptionHandler invoked exc.message=[${throwable.message}]👍 ") }
      + CoroutineName("main-scope")
  )
 
  val async = mainScope.async(CoroutineName("async-coroutine")) {
    myPrint(" [async-coroutine]: I am going to throw in about 100ms")
    delay(100)
    myPrint(" [async-coroutine]: Throwing exception now!")
 
    throw RuntimeException("I am an exception from async coroutine")
  }
 
  val waiter = mainScope.launch {
    try {
      myPrint("   [launch-routine-just-waiting] I am going to wait 500 millis and then await")
      delay(500)
      async.await()
    } catch (e: CancellationException) {
      myPrint("   [launch-routine-just-waiting] I was cancelled! CancellationException.message=[${e.message}]")
      throw e
    } catch (e: Exception) {
      myPrint("   [launch-routine-just-waiting] ✅ I caught an exception! Exception.message=[${e.message}]")
    }
  }
 
 
  runBlocking {
    mainScopeJob.join()
  }
 
  myPrint("[MAIN] DONE.")
}

Command to reproduce:

gt.sandbox.checkout.commit 1b939780478f3d10a020 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[  7 ms] [MAIN] START...
[ 43 ms]    [launch-routine-just-waiting] I am going to wait 500 millis and then await
[ 43 ms]  [async-coroutine]: I am going to throw in about 100ms
[145 ms]  [async-coroutine]: Throwing exception now!
[167 ms]    [launch-routine-just-waiting] I was cancelled! CancellationException.message=[Parent job is Cancelling]
[168 ms] [MAIN] DONE.