CoroutineExceptionHandler only works with launch

CoroutineExceptionHandler processes uncaught exception from launch

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Emojis
import gt.sandbox.util.output.Out
import kotlinx.coroutines.*
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds

fun main(): Unit = runBlocking {
  val out = Out.standard()
  out.info("START")

  try {
    mainImpl(out)
  } catch (e: Exception) {
    out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1")

    exitProcess(1)
  }

  out.info("DONE - WITHOUT errors on main")
}

private suspend fun mainImpl(out: Out) {
  val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
    runBlocking {
      out.info("${Emojis.CHECK_MARK} CoroutineExceptionHandler CALLED!")
      out.info("CoroutineExceptionHandler is called with throwable: ${throwable::class.simpleName} - ${throwable.message}")
    }
  }

  val parentJob = Job()
  val scope = CoroutineScope(parentJob + coroutineExceptionHandler)

  out.info("Launching coroutine that will throw exception...")
  val job = scope.launch {
    out.info("Launch coroutine started")

    out.delayLogsCancellation(500.milliseconds)

    throw MyExceptionWillThrowFromCoroutine.create("Exception from launch", out)
  }

  out.actionWithMsg("job.join()", { job.join() })
  out.infoPrintState(job, "launchJob")
  out.infoPrintState(scope, "scope-where-we-launched")

  scope.cancel()
}

class MyExceptionWillThrowFromCoroutine private constructor(msg: String) : RuntimeException(msg) {
  companion object {
    suspend fun create(msg: String, out: Out): MyExceptionWillThrowFromCoroutine {
      val exc = MyExceptionWillThrowFromCoroutine(msg)

      out.warn("${Emojis.EXCEPTOIN} throwing exception=[${exc::class.simpleName}] with msg=[${msg}]")

      return exc
    }
  }
}

Command to reproduce:

gt.sandbox.checkout.commit a7bc470832057f8c7ff6 \
&& 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:   20ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START
[INFO][elapsed:   40ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Launching coroutine that will throw exception...
[INFO][elapsed:   46ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] Launch coroutine started
[INFO][elapsed:   46ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performing action=[job.join()]
[WARN][elapsed:  582ms][2️⃣][⓶][coroutname:@coroutine#2][tname:DefaultDispatcher-worker-1/tid:31] 💥  throwing exception=[MyExceptionWillThrowFromCoroutine] with msg=[Exception from launch]
[INFO][elapsed:  584ms][2️⃣][⓷][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] ✅ CoroutineExceptionHandler CALLED!
[INFO][elapsed:  585ms][2️⃣][⓷][coroutname:@coroutine#3][tname:DefaultDispatcher-worker-1/tid:31] CoroutineExceptionHandler is called with throwable: MyExceptionWillThrowFromCoroutine - Exception from launch
[INFO][elapsed:  586ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Performed action=[job.join()]
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [launchJob]:
launchJob.isActive    | false
launchJob.isCancelled | true
launchJob.isCompleted | true
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] State of [scope-where-we-launched]: isActive=false
[INFO][elapsed:  589ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - WITHOUT errors on main

Backlinks