Trace how an exception thrown by an awaited operation reaches the suspend function that awaited it, in terms of `resumeWith`, `Result`, and the state machine.
answer
- Single channel: resumeWith(Result<T>)
- Failure = resumeWith(Result.failure(e))
- Re-entry does result.getOrThrow() -> rethrow at suspension point
- Uncaught -> propagate Result.failure to completion (parent)
- Same path carries CancellationException
basics
~20 sFailures travel through the same resume channel as values. The continuation is resumed with a failed Result; when the state machine re-enters, it unwraps that Result, which re-throws the exception right at the line that was awaiting.
solid answer
~40 sThere is one resume channel for both success and failure: `Continuation.resumeWith(result: Result<T>)`. A failed awaited operation calls `resumeWith(Result.failure(e))` (or the `resumeWithException(e)` helper). Inside `BaseContinuationImpl.resumeWith`, the stored `Result` is passed into `invokeSuspend(result)`. The generated state machine's first action on re-entry is effectively `result.getOrThrow()` (i.e., `throwOnFailure`), so a failed Result **re-throws the exception synchronously at the suspension point** — exactly where the awaiting code logically paused. From there normal Kotlin `try/catch` around the suspend call works as expected: the exception propagates through the `when (label)` branch and unwinds. If uncaught, `resumeWith` delivers `Result.failure` onward to the `completion` (parent) continuation, propagating the failure up the coroutine chain. This unified `Result`-based resume is why structured concurrency can route exceptions to parents and why `try/catch` around `await()`/suspend calls behaves naturally.
code
kotlin · 9 lines// Bridging a failing callback:
suspendCancellableCoroutine<String> { cont ->
api.call(object : Callback {
override fun onError(e: Throwable) =
cont.resumeWith(Result.failure(e)) // surfaces at the await site
})
}
// At the await: state machine re-enters, calls result.getOrThrow(),
// which rethrows e exactly where the suspend call was.go deeper
Knows that exceptions from awaited work surface at the await line and can be caught with try/catch.
Explains the single resumeWith(Result) channel and that failure is delivered via Result.failure.
Traces re-entry -> getOrThrow rethrow at the suspension point and propagation of uncaught failures to the completion/parent.
Connects this to structured-concurrency exception routing, CancellationException semantics, and CoroutineExceptionHandler policy at the scope boundary.
## One channel for value and failure The `Continuation` interface has a single method: ```kotlin public fun resumeWith(result: Result<T>) ``` `Result<T>` is Kotlin's success-or-failure union. So success and failure flow through the **same** resume path; there is no separate error callback. ## Delivering a failure When an awaited operation fails, whoever bridges it calls: ```kotlin continuation.resumeWith(Result.failure(e)) // or the helper: continuation.resumeWithException(e) ``` ## Re-entry and re-throw `BaseContinuationImpl.resumeWith` stores the result and invokes `invokeSuspend(result)`. The compiled state machine, on re-entry at a suspension point, performs the equivalent of: ```kotlin result.getOrThrow() // ResultKt.throwOnFailure(result) ``` If the `Result` is a failure, this **throws the exception right there** — synchronously, at the exact `when (label)` branch corresponding to the awaited call. To the source code it looks as if the suspend call itself threw: ```kotlin suspend fun load(): Data { return try { remote.fetch() // if this fails, the exception surfaces here } catch (e: IOException) { // ordinary try/catch works fallback() } } ``` ## Propagation up the chain If the suspend function does not catch it, the exception unwinds its state machine and its own `resumeWith` reports `Result.failure(e)` to its **`completion`** continuation (the parent). This walks the continuation chain upward, which is how structured concurrency delivers child failures to the parent `Job`/scope (where cancellation and `CoroutineExceptionHandler` policies apply). ## Why this matters - `try/catch`, `finally`, and even `use {}` around suspend calls behave like synchronous code — because the rethrow happens inline at the suspension point. - `CancellationException` rides the same channel; it's resumed-with-failure and rethrown, which is how cancellation cooperatively interrupts suspended code. ## Terms to name `resumeWith(Result<T>)`, `Result.failure` / `resumeWithException`, `getOrThrow` / `throwOnFailure`, `invokeSuspend`, `completion`, `CancellationException`.
- Why does ordinary `try/catch` around an `await()` or suspend call work even though the work happened on another thread?Because the failure is delivered as Result.failure and rethrown synchronously at the suspension point during re-entry, so from the source's perspective the suspend call itself threw — inside the try block.
- How does a child coroutine's uncaught exception reach its parent in this model?The child's resumeWith reports Result.failure to its completion (parent) continuation, walking the continuation chain upward to the parent Job/scope where cancellation and handler policies apply.
Like a return-mail slot that accepts both a paid invoice and a 'payment bounced' notice through the same opening — the recipient opens it and reacts to whichever it is.
saying these in an interview costs you the question
- Thinks there's a separate error callback distinct from resumeWith
- Believes exceptions are swallowed or returned as values silently
- Doesn't mention Result.failure / getOrThrow rethrow at the suspension point
- Claims try/catch around suspend calls cannot work across threads
- Confuses resumeWith failure with a crashed thread