Why can a web framework's error mapper not turn a failure into a 500 once the response has been committed?
answer
- first flush is the point of no return
- status already on the wire
- mapper replaces, it cannot amend
- a clean ending reads as success
- buffering keeps the status unchosen
basics
~20 sOnce the status line and headers are flushed to the socket, nothing can retract them; HTTP has no take-back. A mapper running afterwards can only append to the body, emit a trailer, log the failure, or abort the connection.
solid answer
~50 sA response is **committed** the moment the framework writes the status line and header fields out to the network. From then on, the reply the client is already parsing has declared its status and its content type, and the central exception-to-status mapper — whose whole job is to *choose* a status and write a failure body — has nothing left to choose. Ask it to run anyway and most frameworks raise a secondary `response already committed` error or drop the rewrite; the worst write the failure body *after* the good bytes, producing one corrupt document. What remains is narrow: stop writing and abort so the receiver sees an incomplete message, optionally carry a reason in a trailer if the response was framed for one, and record the failure server-side. Buffering the body until the handler finishes is what keeps the mapper usable at all.
go deeper
Remember the shape: a response has a status line and headers first, a body second, and once the first part has been sent it is fixed. An error after that cannot change the status code the client already received.
Explain commit as the first flush of the buffer, and explain why a status-choosing mapper is meaningless afterwards. Name what is still available: append, trailer, abort, log — and why appending an error document corrupts the payload.
Show that you have debugged a half-sent success: the access log shows 200, the alert never fires, the client stored a short result. Talk about counting these separately and about aborting rather than finishing cleanly.
Frame it as a design constraint on the API surface: which responses may stream at all, what the consumer contract says about incompleteness, and how much memory the platform is willing to spend to keep failures mappable.
## What `committed` means on the error path A server-side web framework assembles a reply in two halves: a **status line plus header fields**, then a **body**. Those halves are not necessarily held in memory until the handler returns. Most frameworks write into a bounded output buffer, and the head of the response is pushed to the socket the first time that buffer fills, the first time the handler explicitly flushes, or the moment a streaming write begins. That first push is **commit**: the point at which the status code and headers stop being a plan and become bytes the peer has received. Every piece of the framework's error machinery — the mapper keyed by exception type, the shared failure-body factory, the last-resort fallback that catches what nothing else did — is built on the assumption that it runs *before* that moment. Its contract is: exception in, `(status, headers, body)` out. That contract is only satisfiable while no part of the response has been sent. ## Why the mapper has nothing left to choose HTTP gives a server exactly one final status line per request and no frame that means *ignore what I just said*. A `200 OK` on the wire is a promise the protocol will not let you withdraw. So the mapper's substitution — replace the whole response with `500` and a failure document — is not merely discouraged after commit, it is unrepresentable. Frameworks differ in how they react when handler code fails at that point. Some raise a secondary error stating the response is already committed and then swallow it, because there is nowhere left to report it. Some let the rewrite proceed against the output stream, which appends the failure body **after** the partial success body and hands the client one corrupt payload. Some simply close the connection. The one behaviour none of them can offer is a changed status. ## What is actually still available | Action after commit | What the receiver can observe | Cost | |---|---|---| | Stop writing, abort the connection | The body ends before its declared length or terminator arrives, so parsing or transport fails | The connection is spent, and the client learns only *that* it failed, not why | | Finish the message normally, log server-side | Nothing at all — a complete, successful-looking response | Silent truncation; the client stores a short result as if it were whole | | Emit an error trailer | A reason field after the body, if the framing carries trailers and the client reads them | Many clients and intermediaries drop trailers, so it cannot be the only signal | | Write an in-band sentinel into the body | Only if the media type and the consumer already agree on one | Needs a contract designed up front, not improvised at failure time | | Record the failure server-side | Nothing client-side | None; this is the only always-available option | ## The three failure modes teams actually ship 1. **Log and swallow.** The exception is caught near the write loop, written to a log, and the response is terminated normally. The client believes it got everything. This is the default shape of the bug because it is the shape of a well-meaning `try/catch` around a write. 2. **The double write.** The generic error handler runs, serialises a failure document, and appends it to a body that already contains half a success. Neither half parses. 3. **The lying access log.** The request is recorded with the status that was committed. A dashboard built on response status shows a clean `200`, so an incident that users can see is invisible to the people on call. ## Keeping failures mappable in the first place The structural fix is to move risk **before** the first flush: - **Buffer when you can.** If the whole body fits a bounded buffer, nothing is committed until the handler returns, so an exception still lands on the mapper and the client gets a proper status. - **Do the dangerous work early.** Authorisation checks, lookups that can miss, and serialisation of the object graph are the usual sources of late exceptions. Run and materialise them before writing the first byte. - **Choose the status from a complete result.** A handler that decides `200` and then discovers the result is empty has already given away its ability to say `404`. - **Treat streaming as an explicit trade.** Streaming buys bounded memory and early first-byte, and it spends the ability to change your mind. That is a fine trade for large or long-lived responses and a poor one for a small document. The interview point is not that after-commit failures should never happen — for genuinely streamed responses they always remain possible. It is that a candidate should know the status is frozen, know that a cleanly finished body is a false success signal, and know that buffering is the lever that decides how much of the response is still under the mapper's control.
- If the mapper cannot change the status, what should the framework do with the exception it caught after commit?Record it server-side with the request's correlation identifier, count it on a metric that is separate from ordinary handler errors, then stop writing and abort rather than terminate the message normally. Everything else is optional; a cleanly finished body would hide the failure from both the client and the dashboards.
- Does writing the failure body after the already-sent bytes ever help the client?Rarely. The bytes land inside the same message, so the client parses one document made of half a success and a whole error. Unless the consumer was designed to scan for a sentinel, it either fails to parse or, worse, parses a plausible fragment. Aborting is the more honest end.
- How does buffering the response change what is possible?With a bounded buffer that the handler never overflows, no bytes reach the socket until the handler returns, so the status is still unchosen and the mapper works exactly as it does for any other failure. The cost is memory proportional to the body and a later first byte.
It is a live broadcast: once you have announced the winner on air you cannot un-announce it. You can keep talking, cut the feed, or correct the record afterwards — but the announcement went out.
saying these in an interview costs you the question
- Claims the server can still send a 500 after a 200 has gone out
- Thinks appending the error body after the good bytes repairs the response
- Assumes the exception mapper runs for every failure regardless of timing
- Treats a truncated body as harmless because the status said 200
- Believes ending the message normally after a failure still signals an error
- Says nothing can be done, so the exception is simply discarded