skip to content

In a middleware chain, why is a hook's after-delegate code skipped when an inner element throws?

level: middleimportance: must knowfreq 58%

answer

  1. the delegate call never returned
  2. after-code assumes success
  3. cleanup always, catch optionally
  4. recovery stops the unwind

basics

~20 s

Because the delegate call never returned normally: the unwind jumps past the statements that follow it. Only a cleanup construct around the call still runs, and only a catch lets the hook observe the failure at all.

solid answer

~40 s

An after-phase is not a callback the framework invokes on both paths — it is just the statements written after an ordinary call, and they run only when that call returns. When the inner part fails, the call unwinds instead of returning, so those statements are skipped. There are three useful shapes around a delegate call. A **cleanup construct** runs on both paths and is where releasing, unlocking and clearing ambient context belongs. A **catch that records and rethrows** observes the failure without changing it. A **catch that produces a result** is recovery: it stops the unwind there, so every element outside sees an ordinary success and the error stage never fires. Choosing between them is the whole decision.

go deeper

for a junior

Remember that code after a call runs only if that call returned. A failing inner element means the lines you wrote below the delegate call are simply never reached.

for a middle

Be able to name the shapes: plain after-code, cleanup that runs either way, a catch that records and rethrows, and a catch that recovers and stops the failure there.

for a senior

Demonstrate the operational consequence — metrics and spans finished after the call describe only successes, so the data looks best when the system is worst.

for a principal

Own the rule for the estate: where recovery is allowed at all, how it must be made visible, and how you stop a convenience catch from quietly becoming the error policy.

An element in a middleware chain usually reads: do some work, call the next element, then do some more work with what came back. Those halves get called the **before-phase** and the **after-phase**, and the naming hides the point — in most frameworks the after-phase is not a hook the framework calls on your behalf, it is simply the code that follows an ordinary call. Statements after a call run when the call **returns**. If the inner part fails, the call does not return; it unwinds, and the unwind travels past those statements into whatever the language provides for cleanup or catching. ## The shapes you can write around a delegate call | Shape | Runs on the failure path? | Effect on the error | Typical use | |---|---|---|---| | Statements after the call | No | none | after-phase work that assumes success | | Cleanup construct around the call | Yes | none — the error keeps going | release, unlock, clear context, stop a timer | | Catch that records and rethrows | Yes | none — the error keeps going | logging, tagging, enriching the failure | | Catch that produces a result | Yes | the unwind stops here | deliberate recovery or degradation | Rows two and three are often confused. Cleanup is about **resources**: it must happen whatever the outcome, and it makes no decision. A recording catch is about **observation**: it learns that a failure occurred and deliberately lets it continue. Neither changes the outcome of the request. ## Recovery ends the story for everyone outside The fourth row is different in kind. When a hook catches and produces a response or a value instead of rethrowing, the failure stops existing as far as the rest of the chain is concerned. Concretely: - Each enclosing hook's delegate call **returns normally**, so its after-phase statements run as if nothing went wrong. - The outer error stage is never reached, so nothing error-shaped is logged, counted or reported by it. - The client may receive a success status for a request whose real work failed. That is exactly what recovery is for when the concern is optional, and exactly why a broad catch placed for convenience is dangerous. The test to apply is simple: after this catch, does anyone outside still know something went wrong? If the answer is no, the catch is a decision to hide a failure, not a piece of resilience. ## The measurement trap The most common concrete bug from this rule is measurement. A hook that starts a timer, delegates, and records the duration afterwards records **only the requests that succeeded**. The same applies to incrementing a counter, adding a response header, recording a response size, or finishing a trace span. The result is survivorship bias in the worst possible direction: the requests that are missing from the data are the failing and often the slowest ones, so dashboards look healthiest exactly when the system is least healthy. Moving the recording into a construct that runs on both paths, or into a catch that records and rethrows, fixes it and usually costs one line. ## Cleanup is not handling Two more traps sit near cleanup code. 1. **Cleanup that itself fails.** In several execution models an error raised inside the cleanup path can displace the original error, so the failure you finally see is the secondary one and the real cause is gone. Keep cleanup trivial and guard anything in it that can fail. 2. **Cleanup used as handling.** Releasing a connection is not deciding what the client is told. A hook that only cleans up must still let the failure continue outward; if it does not rethrow, it has silently become a recovery point. ## A checklist for writing a hook - Ask, for each line after the delegate call: is it acceptable for this to be skipped when the request fails? - Anything that must be symmetric with a before-phase action belongs in cleanup, not after the call. - Log or count in a catch that **rethrows**, so observation never becomes recovery by accident. - If you recover, make the recovery itself observable — a counter or a log line — so a persistent inner failure cannot hide behind a healthy-looking response. - Test the hook by making the inner element throw, not only by making it succeed; the failure path is the one that is never exercised by accident.

  • What changes for the enclosing hooks if one hook catches and recovers?
    They see a normal return carrying whatever the recovering hook produced, so their after-delegate code runs as though nothing failed. The unwind stops there, the outer error stage is never reached, and error-only logging and counters never fire. That is the purpose of recovery and also the risk of catching broadly.
  • Where should per-request timing live so that failures are counted?
    Around the delegate call in a construct that runs on both paths, or in a catch that records and rethrows. Statements after the call capture successes only, which biases latency and error-rate data toward the healthy path exactly when failures matter most.
  • Can cleanup code hide the original failure?
    It can. In several execution models an error raised while the cleanup path is running replaces the one that was unwinding, so the outer elements see the secondary failure and the real cause disappears. Keep cleanup trivial and guard anything inside it that might fail.

saying these in an interview costs you the question

  • Thinks the after-phase is a callback the framework invokes on both paths
  • Records response times after the delegate call and loses every failed request
  • Catches broadly so logging keeps working, silently swallowing the failure
  • Assumes releasing a resource after the delegate call is safe
  • Believes a hook that recovers still lets the outer error stage see the error