When a handler returns an awaitable that fails or never completes, how does a web framework turn that into a response?
answer
- two channels: thrown early, failed late
- a sync catch misses a failed handle
- same error stage, different arrival
- never settles means nothing is written
- started minus completed, not error rate
basics
~20 sA failed awaitable is delivered to the framework's error-mapping stage, the same one that handles a thrown error, and becomes an error status. An awaitable that never settles produces no response at all: the exchange stays open holding its resources.
solid answer
~40 sTwo channels exist, and they reach the framework at different moments. An error thrown *before* the handler returns takes the synchronous path; an error carried by the returned handle arrives later, through the completion callback. A framework that supports awaitable handlers feeds both into the same error mapping, so a failed handle becomes a status and an error body just as a thrown error does — provided the failure is actually inside the handle the handler returned. Failures that are swallowed, or that settle a handle the handler never returned, have nowhere to land and surface only as unhandled-error logs. A handle that never settles is worse: there is no callback, so the framework writes nothing and the request sits open until something outside this path closes it.
go deeper
Remember the three outcomes of a returned handle: a value, a failure, or nothing at all, and that only the first two ever produce a response.
Be able to explain the two error channels and why a synchronous guard around the handler call covers only the earlier one, so failure mapping has to compose onto the handle.
Demonstrate that you monitor for the silent case: requests started versus responses written, and the age of the oldest in-flight request, because a handle that never settles emits no error at all.
Argue for one error-mapping stage covering both channels as a platform decision — per-handler error handling drifts, and divergent mappings show up as inconsistent status codes across a service surface.
## Three outcomes, not two Once a handler has returned a handle to the framework, the request can end in one of three ways. 1. **The handle completes with a value.** The framework maps that value to a status, headers and a body exactly as it would map a synchronous return. 2. **The handle completes with a failure.** The framework hands the error to its error-mapping stage and writes an error response. 3. **The handle never settles.** No callback fires, so the framework has nothing to write. This is the outcome people forget, and the one that leaks. ## Two error channels, and why they are not the same The work of an awaitable-returning handler is split in time: a synchronous prologue that runs before the return, and asynchronous work that runs after it. | | error thrown before the return | failure carried by the handle | |---|---|---| | when it happens | while the framework is still inside the call | after the handler returned | | how the framework learns of it | the call throws | the completion callback receives it | | what must be wired up | nothing; it is the ordinary path | the framework must be subscribed to that handle | | typical cause | argument binding, a guard, a null lookup | a downstream call, a timeout inside a composed step | A framework that supports awaitable handlers normally funnels both into **one** error-mapping stage, so the same mapping rules produce the same response shape either way. The practical consequence is that an error-catching wrapper written as `try { next(request) } catch { … }` around the call catches only the first channel: by the time the failure settles, the wrapper has long since returned. On the asynchronous path the wrapper has to attach itself to the handle instead — `return next(request).onFailure(map)` in shape — or let the framework's own error stage do it. ## Where failures go missing - **The handle was never returned.** A handler that starts work and returns some other value hands the framework a finished request; the failure later settles a handle nobody watches, and it appears only as an unhandled-error log line. - **The failure was swallowed inside a composition step.** A recovery step that maps every failure to a default value is indistinguishable, from outside, from a call that succeeded — including the failures you did want mapped to an error status. - **A second failure arrives after the first.** A handle settles once. Work fanned out from the handler that fails afterwards has no channel left; its error goes to a global unhandled-failure hook if the runtime has one, and to nothing if it does not. - **The failure is not the type the error stage keys on.** Error mapping usually matches on error type or on a marker the framework understands; a failure carrying something unexpected falls through to the catch-all mapping, which is almost always a generic 500. ## The handle that never settles This is the most instructive failure because nothing at all happens. There is no error, no log line and no response — only a request that never ends. Typical causes are a composition whose last step forgets to complete the handle on one branch, a callback-to-handle bridge whose error path never calls the completion, and a chain that ends by discarding its own result. What the request holds while it is stuck is the real cost: an open connection, whatever per-request state the framework allocated, a slot in any in-flight limit the server enforces, and a row in the connection table. Multiply by request rate and the process runs out of one of those long before anyone notices the absence of errors. Detection is by absence: count requests started against responses written, and alert when the gap grows — a per-request error rate shows nothing, because no error was ever produced. (What eventually closes such a request, and on whose clock, is a separate mechanism with its own rules.) ## What good answers do - Let failures **propagate** as failures of the returned handle rather than converting them to sentinel values inside the handler. - Map error types to statuses in the framework's error stage, not by a catch inside each handler, so both channels behave the same. - Make every composition path complete the handle, including the branch nobody expects to run. - Watch started-minus-completed as a health signal, because a stalled handle is invisible to error-rate monitoring.
- Why does a try/catch wrapped around an awaitable-returning handler call not catch the failure?Because the handler returns successfully. The catch covers only the window in which control is inside the call, and the failure settles the handle after that window closed. To cover the asynchronous path, the wrapper has to compose onto the returned handle — attaching a failure-mapping step and returning the composed handle — rather than guarding the call.
- What should a handler do with a failure it can map to a meaningful status, like a missing record?Represent it as a failure of the returned handle carrying an error type the framework's error stage recognises, or complete the handle with an explicit response value for that status. What it should not do is quietly substitute a default value: that is indistinguishable from success and turns a 404 into a 200 with empty content.
- How would you detect requests whose handle never settles?Compare requests admitted against responses written, and track the age of the oldest in-flight request. Both grow monotonically when handles stall. Error rate and latency percentiles do not move, because a request that never answers contributes neither an error nor a completed sample.
saying these in an interview costs you the question
- Thinks a catch around the handler call sees a failed handle
- Says a failed awaitable always produces a 500 regardless of mapping
- Recovers every failure to a default value inside the handler
- Assumes an unsettled handle eventually fails on its own
- Believes error-rate metrics reveal stalled requests
- Thinks a handle can deliver more than one outcome