skip to content

When a middleware chain's delegation call returns a pending result rather than a finished response, where does the after phase belong?

level: seniorimportance: nice to knowfreq 44%

answer

  1. the call returns a handle, not a response
  2. attach the after phase to completion
  3. return the composed result outward
  4. near-zero timings are the tell
  5. capture context, do not assume it

basics

~20 s

Attach it to the completion of the pending result and return the composed result outward. Code written as the next statement runs while the inner work is still in flight, so it sees no response and releases resources too early.

solid answer

~50 s

In a non-blocking pipeline the delegation call hands back a handle that completes later, so *after the call returns* no longer means *after the inner work finished*. The after phase therefore moves into a continuation attached to that handle, and the element returns the composed handle so its own caller waits for the whole thing rather than for the call to be made. Three things go wrong when it is written inline instead: logs and metrics record an absent or default response, timings come out near zero, and cleanup runs while the handler is still using what it released. A fourth is subtler, which is making the call and returning nothing, so the outer elements believe the request is finished. Anything the continuation needs should be captured explicitly rather than read from ambient per-thread state, which may not be in place when it runs.

go deeper

for a junior

Know that the delegation call does not always hand back a finished response; in some frameworks it returns a handle that completes later.

for a middle

Explain the fix in mechanism terms: attach the after-phase work to the completion of the returned result and return the composed result so callers wait for it.

for a senior

Recognise the symptoms in production, such as impossibly fast timings, empty statuses in access logs and resources used after release, and trace them back to an inline after phase.

for a principal

Decide whether a codebase mixes execution styles at all. Allowing blocking elements in a non-blocking pipeline shifts a correctness and capacity risk onto every team writing hooks.

## What returning means when the result is pending The wrapping model is unchanged by the execution style: an element still holds one callable representing the remainder, still invokes it once, and still returns a result outward. What changes is **when the inner work is done**. In a blocking style the delegation call does not return until the remainder has finished, so the statement after it is genuinely the after phase. In a non-blocking style the call returns almost immediately with a handle, a future, a promise or a suspended computation, that will complete later. The statement after the call now runs while the handler has barely started. | Style | What the call returns | What *after the call* means | |---|---|---| | thread-per-request, blocking | the finished response | the inner work is complete | | non-blocking, deferred result | a handle that completes later | the inner work has merely been started | ## How the inline after phase fails Writing the after phase as the next statement in a deferred pipeline produces failures that look unrelated to each other until you know the cause: - **Observability lies.** An access log records a missing or default status and a zero byte count, because no response exists yet. Duration metrics collapse to microseconds and everything looks wonderfully fast. - **Cleanup lands too early.** Anything the element releases, closes or clears after the call is taken away while the handler is still using it, which surfaces as intermittent failures under load rather than as a clean error. - **The result is dropped.** If the element calls the remainder and returns nothing, or returns a value of its own instead of the composed handle, its enclosing elements treat the request as finished. Their after phases run early, and whatever the handler eventually produces may arrive with nowhere to go. This is the deferred version of a missed delegation. ## The shape that works ``` element(request, next): started = now() pending = next(request) # returns immediately return pending.on_complete(response -> # after phase, attached record(now() - started, response.status) response ) ``` Two details carry the whole idea. First, the after phase is **attached** to the completion rather than written below the call. Second, the element **returns the composed handle**, not the original one and not nothing, so that the element outside it is waiting on something that finishes only when this element's own after-phase work is done. Chain those two rules through every element and the outer world sees exactly the same semantics as a blocking pipeline. ## State that does not travel by itself A continuation may run on a different carrier thread from the one that started the request. Two habits follow: 1. **Capture what you need into the continuation** as plain values, such as the start time and the correlation value, instead of reading them from ambient per-thread storage inside it. 2. **Do not assume ambient context is restored.** Some runtimes propagate it, some do not, and relying on it produces logs that lose their correlation value only under load, when tasks actually change threads. ## Mixing styles inside one pipeline An element written in the blocking style inside a non-blocking pipeline does not merely slow itself down. If it waits for the pending result on a shared event-loop carrier, it occupies a resource that many other requests depend on, and throughput collapses well before anything looks broken in that element's own metrics. The reverse mistake, treating a finished response as if it were pending, is harmless by comparison. ## How to tell which style you are in Look at what the delegation call gives back. If it is a response you can read a status from, the code after it is the after phase. If it is a handle with a way to attach completion work, the code after it is not the after phase and must not pretend to be. Two cheap checks make the answer obvious: - Read the type of the delegation call's result. A status you can read means blocking; something you attach completion work to means deferred. - Read the element's own return statement. If it returns a value that is not derived from the delegation result, the element has stopped being part of the request's completion. A useful review question for any element is simply: *what does the element return, and is it the thing that completes when everything inside has finished?* ## Why this is a senior question It is rarely asked as theory. It shows up as a report that response times are unbelievably good, or that a log line always shows an empty status, or that a resource is occasionally used after release. Recognising the pattern behind those three symptoms, and knowing that the fix is composition rather than another lock or another retry, is the point of asking.

  • What symptom most often reveals an after phase written inline in a deferred pipeline?
    Measurements that are too good. Durations of microseconds for real work, and access logs with an empty or default status and zero bytes, because the element recorded them while the inner work was still in flight rather than when it completed.
  • Why is returning the composed result as important as attaching the continuation?
    Because the enclosing element waits on whatever this one returns. Return the original handle and your own after-phase work is not part of what the outer element awaits; return nothing and the outer elements treat the request as finished while the handler is still running.
  • Why should a continuation not read the correlation value from ambient per-thread storage?
    Because it may run on a different carrier thread from the one that started the request. Some runtimes propagate such context and some do not, so the reliable approach is to capture the value as a plain variable when the element still has it and use that captured copy.

saying these in an interview costs you the question

  • Assumes the delegation call always returns a finished response
  • Logs the response on the statement after the call in a deferred pipeline
  • Releases resources after the call without waiting for completion
  • Calls the remainder but returns its own value instead of the composed result
  • Waits for the pending result on a shared event-loop carrier