Why is this CoroutineExceptionHandler never called on the failure?
A handler is attached so that a failed load is logged instead of crashing the app. The coroutine does fail, the app crashes, and the handler is never invoked.
Constraints: the failure must reach the handler, viewModelScope must stay the owner of the work, and the handler must not be duplicated at every call site.
private val handler = CoroutineExceptionHandler { _, e -> Log.e("vm", "load failed", e) }
fun load() = viewModelScope.launch {
launch(handler) {
throw IOException("boom")
}
}
Diagnose the cause and fix it.
A handler fires only on the coroutine that handles the failure — the scope's root. On a child it is ignored: the exception is passed up to the parent. Install it in the scope's context. async never calls it — await() rethrows.
- ✗Installing the handler on a child coroutine instead of the scope's root
- ✗Expecting a handler to fire for a failure raised inside
async - ✗Thinking a
try/catcharoundlaunchcatches what the child throws
- →What does a
SupervisorJobchange about which coroutine handles the failure? - →Where does the exception go when no handler is installed at all?
The cause
A CoroutineExceptionHandler is not a "try/catch for a coroutine". It is invoked only by the coroutine that actually handles the failure, and that is the root of the scope. A child handles nothing: it passes the exception up to its parent, so a handler in the child's context is ignored.
fun load() = viewModelScope.launch {
launch(handler) { // handler on the CHILD — ignored
throw IOException("boom")
}
}
The fix
The handler belongs in the scope's context (or on the root launch):
private val vmScope = viewModelScope + CoroutineExceptionHandler { _, e ->
Log.e("vm", "load failed", e)
}
fun load() = vmScope.launch { // the scope's root — the child's failure arrives here
launch { throw IOException("boom") }
}
Two consequences interviewers follow up on: under a SupervisorJob the failing coroutine handles its own failure, so a handler on that "child" does fire; and async never invokes a handler at all — its exception is stored in the Deferred and rethrown from await().