skip to content

In an app that mixes synchronous handlers with awaitable-returning ones, what breaks in shared wrappers and request-scoped state?

level: seniorimportance: should knowfreq 50%

answer

  1. the return is a handle, not a response
  2. after-work runs too early
  3. timers and cleanup land in the wrong place
  4. compose onto the handle instead
  5. ambient state may not survive the wait

basics

~20 s

A wrapper that does its after-work right after calling the next stage runs that work when the handler returns its handle, not when the response exists. Timings, cleanup and error catching all land in the wrong place.

solid answer

~50 s

The wrapper contract changes with the handler shape. Around a synchronous handler, returning from the next stage means the response is ready, so after-work is safe there. Around an awaitable-returning handler, the return means only that the handle exists: a timer stopped there measures the hand-off rather than the request, a cleanup step closes resources the pending work still needs, and a guard around the call never sees a failure that settles later. The fix is to make the wrapper compose onto the returned handle — `return next(request).onComplete(after)` in shape — rather than run its after-part inline. The same boundary hits request-scoped values kept in ambient storage: they are reliably present for a synchronous handler but may be absent or recycled once the work resumes elsewhere. Frameworks that support both shapes usually provide an async-aware wrapper interface, and the two kinds should not be mixed in one chain.

go deeper

for a junior

Hold on to the core fact: with an awaitable-returning handler, code that runs right after calling the handler runs before the response exists.

for a middle

Explain the two wrapper forms and rewrite an around-form stage into one that attaches its after-work to the returned handle so it fires on completion.

for a senior

Diagnose the mixed application in production: implausibly fast converted endpoints, error counters that went quiet, correlation identifiers missing or attached to the wrong request.

for a principal

Make the wrapper audit part of the migration's definition of done, and decide deliberately whether the two shapes live in separate chains or separate deployables — an unowned shared wrapper layer is what turns a conversion into silent instrumentation debt.

## Where the two shapes diverge A wrapper — the generic name for the stages a framework runs around a handler — is usually written in one of two forms: - **Around form**: do something, call the next stage, do something with what came back. - **Compose form**: do something, call the next stage, and attach the after-work to the handle it returned. Against a synchronous handler both forms behave identically, because the return of the next stage *is* the response. Against an awaitable-returning handler they differ completely: the return is a handle, and the response does not exist yet. | after-work in an around-form wrapper | synchronous handler | awaitable-returning handler | |---|---|---| | stop a timer | measures the whole request | measures only the hand-off | | log the status | the real status | none set yet, or a placeholder | | close a per-request resource | safe | closes it under the pending work | | catch a failure | sees handler errors | sees only pre-return errors | | write a header | still uncommitted, works | works, but before the handler decided | The damage is quiet. A latency dashboard fed by around-form timing on converted endpoints shows sub-millisecond responses that no client ever experienced, and the error rate drops because failures are no longer caught where they are counted. Both look like the conversion was a triumph. ## Request-scoped state across the same boundary Frameworks expose per-request values — identity, correlation identifiers, a transaction handle, negotiated locale — to code far from the handler through ambient storage tied to whatever is executing. For a synchronous handler this is dependable for the whole request. On the awaitable path, the work after a wait point may resume somewhere else, and that ambient slot may be empty or may already have been reused by another request. The visible symptoms are the ones that scare people: missing correlation identifiers, an identity lookup that returns nothing, and, in the reuse case, a value belonging to a *different* request. Frameworks differ here: some propagate this context across wait points automatically, some offer an explicit propagation hook, and some do neither. Before a conversion, establish which of the three you are in — the answer decides how much code has to change. ## What to do instead 1. **Compose the after-work onto the handle.** Write wrappers on the async path so they return `next(request).onComplete(after)`; the after-work then runs when the response genuinely exists. 2. **Use the framework's async-aware wrapper interface, if it has one.** Frameworks that support both shapes usually offer a second wrapper contract whose return value is itself a handle. Registering a synchronous wrapper on an asynchronous chain is the defect in one line. 3. **Do not mix the two kinds in one chain.** One around-form wrapper in an otherwise composed chain re-creates every symptom above for all handlers behind it. 4. **Pass what you need explicitly at the hand-off.** Where the framework does not propagate context, capture the values into the composed step rather than looking them up later. 5. **Convert a slice, not a signature.** Change the handler, the wrappers on its path, and its dependencies together; a handler whose chain still contains around-form stages is converted in name only. ## Diagnosing an app that is already mixed - Compare the duration a wrapper reports with a client-side or server-edge measurement for the same endpoint. A large, one-sided discrepancy on exactly the converted endpoints is the signature. - Check whether errors from converted endpoints appear in the wrapper's error counters at all; a counter that has gone silent is the finding, not the absence of errors. - Look for logs missing correlation identifiers, and for identifiers appearing on a request they do not belong to. - Audit which wrapper interface each stage implements, endpoint by endpoint. This is usually a short list and usually where the answer is. ## Why teams end up here Mixing is rarely a decision; it is the residue of an incremental conversion. Handler signatures change first because they are the visible part, while the wrappers — shared, older, and owned by nobody in particular — stay as they were. The conversion looks finished and the instrumentation quietly stops describing reality, which is why an endpoint-by-endpoint migration needs the wrapper audit built into its definition of done.

  • How can latency dashboards improve after converting endpoints when nothing got faster?
    Because an around-form wrapper stops its timer when the handler returns its handle, which happens almost immediately. It is measuring the hand-off rather than the request. The fix is to record the duration in a step composed onto the handle, so the timer stops when the response is actually produced; the numbers then return to reality.
  • A wrapper closes a per-request resource after calling the next stage. What goes wrong on the async path?
    The close runs while the pending work still holds the resource, so the work fails on a closed handle — often intermittently, since it depends on whether the work had already finished. Move the close into a completion step attached to the returned handle, so it runs after the response, whether the handle succeeded or failed.
  • Is it acceptable to keep both handler shapes in one application long-term?
    Yes, if the chains are kept separate: each shape gets wrappers written against its own contract and the two never share a chain. What is not sustainable is one shared set of around-form wrappers serving both, because every stage then behaves correctly for one half of the surface and silently wrongly for the other.

saying these in an interview costs you the question

  • Assumes a wrapper's after-work runs after the response on both shapes
  • Stops a request timer when the handler returns its handle
  • Closes per-request resources right after calling the next stage
  • Trusts a synchronous guard to catch failures on the async path
  • Expects ambient request state to survive every wait point
  • Registers one wrapper kind for both chains and calls it shared code