skip to content

Failure Body Contract

Enforcing one error body service-wide: a single response factory all handlers call, framework-raised failures wrapped alike, correlation id from context. Asked because per-handler shapes drift.

on this pageshow

questions

5

In a web framework, why should every failure response be built by one shared factory rather than by each handler?

level: middleimportance: must knowfreq 62%

answer

  1. one shape, many handlers
  2. convention drifts; a call site cannot
  3. handlers raise failures, do not build bodies
  4. single write path stamps id, strips detail
  5. one change point, one test point

basics

~20 s

A single factory is the only place that can guarantee every failure leaves with the same fields, a correlation id and no internal detail. Per-handler bodies drift as soon as a second person adds an endpoint under time pressure.

solid answer

~50 s

A failure body contract is a promise about every non-success response, and a promise held only by convention is broken by the next handler somebody writes in a hurry. So handlers do not build error responses at all: they raise a typed failure, and one factory at the write boundary turns it into the body. That factory owns the field names and nesting, stamps the correlation id it reads from request-scoped context, picks the client-facing message from the mapped error code, and keeps the exception's own text out of the payload and in the log record instead. The payoff is that the contract has one change point, one test point and one audit point. When the shape has to change, you edit one function rather than grepping for every place a map literal was assembled inline.

go deeper

for a junior

Remember the rule in one line: handlers raise a failure, one shared function turns it into the response body. If you are typing field names for an error payload inside a handler, you are writing the bug.

for a middle

Be able to list what the factory owns - field names, the correlation id read from request context, the message chosen from the code, the exclusion of internal detail - and what it leaves to the caller, which is naming the failure and its status.

for a senior

Show how you would make the bypass structurally impossible rather than merely discouraged: a handler return type that cannot express a raw failure response, plus a test that provokes a failure per route and parses the result against the contract.

for a principal

The tradeoff to own is centralisation cost. One write path is a shared dependency every team edits; decide what is fixed envelope, what is declared extension, and how a team ships an endpoint-specific detail without renegotiating the contract for everyone.

A **failure body contract** is the promise that every non-success response a service emits carries the same fields, in the same shape, whatever went wrong and wherever in the code it went wrong. Writing the contract down is cheap. Keeping it is a code-structure problem, and it has exactly one structural answer: either there is **one place in the process that builds a failure body**, or there are as many places as there are handlers. ## Why a documented format is not a mechanism A convention recorded in a wiki page is enforced by whoever remembers it at the moment they are writing code. Over a few months a service that started with one shape reliably accumulates: - a handler that returns a bare string because the failure felt obvious; - a second team that spells the field `errorCode` where the first spelled it `code`; - one endpoint nesting the message under an `error` object, another putting it at the top level; - a field added for one client that quietly becomes part of nobody's contract; - an endpoint whose 'message' is the text of whatever was thrown at it. None of these is a bad decision in isolation. They are the predictable output of a rule that has no call site. The client then grows a branch per endpoint, and the log pipeline cannot parse failures uniformly because the field it keys on is missing from a quarter of them. ## What the factory owns and what it does not | Concern | Owned by the shared factory | Left to the caller | |---|---|---| | Field names and nesting | Yes, exclusively | — | | Correlation id | Read from request-scoped context at write time | — | | Client-facing message | Derived from the mapped error code | Choosing the code | | Stack trace, cause, internal identifiers | Routed to the log record, never the body | — | | Serialized wire format | Rendered at the one write point | — | | HTTP status | — | The caller's failure-to-status mapping | The split matters. The factory is not a god object that decides what went wrong; it decides only how a failure *looks on the wire*. The caller still says which failure this is. ## The call-site shape Handlers stop constructing responses. They raise a typed failure carrying a stable code and whatever structured detail belongs in the body, and the failure is rendered once on the way out: ``` handler(request): order = orders.find(request.path["id"]) if order is null: raise NotFound(code = "ORDER_NOT_FOUND") return ok(order) # one place, at the edge of the chain render_failure(failure): return response( status = status_for(failure), body = failure_body( code = failure.code, message = message_for(failure.code), correlation_id = context.correlation_id())) ``` The important property is not the shape of the pseudocode but that `failure_body` has **one definition and many callers**, and that no handler can reach the response builder without passing through it. ## What one write path buys 1. **One change point.** Adding a field, renaming one, or moving to a different wire format is an edit in one function plus its tests, not a repository-wide search for map literals. 2. **One test point.** You can write a test that provokes a representative failure per class and asserts the parsed body against the contract, and a new endpoint inherits that guarantee instead of needing its own assertion. 3. **One audit point.** The question 'can an internal identifier reach a client?' has a single place to answer, because there is a single place where a body is assembled. 4. **One place for the id.** The correlation id is stamped by the factory from ambient request context, so no handler can forget it and none has to thread it through its own signature. ## Where it gets hard - **Failures raised outside handler code.** A request that matches no route, or a body that cannot be parsed into the declared type, never runs your handler, so it will not pass through your factory unless you deliberately attach the framework's central failure hook to it. - **Work that has left the request's execution unit.** Once handling has hopped to another thread or task, the ambient context the factory reads may be empty unless the framework propagates it, so the factory needs a defined behaviour for a missing id rather than an exception. - **Infrastructure in front of the application.** A proxy or gateway that rejects a request before it reaches the process emits its own body; your factory cannot reach it, and making that boundary consistent is a deployment-level task, not a code-level one. The general rule that survives all three: the contract is only as strong as the narrowest point every failure is forced through. Design that point first, then make it impossible to bypass.

  • If the factory owns the body, what does a handler still decide about a failure?
    Which failure it is. The handler raises a typed failure carrying a stable machine-readable code and any structured detail that legitimately belongs to the caller. Everything downstream of that - field names, message text for the code, the id, what is withheld - belongs to the factory. Keeping the handler's job to naming the failure is what stops the contract from being renegotiated per endpoint.
  • How would you stop a new handler from bypassing the factory and writing its own error body?
    Make the bypass hard rather than forbidden. Give handlers a return type that cannot express a raw failure response, so producing one means raising a typed failure. Back that with a test that provokes a failure on every registered route and asserts the parsed body matches the contract, and with a review rule that a literal error payload in handler code is a defect.
  • Does one shared factory mean every endpoint must return identical fields?
    No - it means the envelope is identical and extensions are declared. A stable core (code, message, correlation id) appears on every failure; endpoint-specific detail goes into one designated place in that envelope with a documented shape. The failure is if the extra detail is added by inventing a new top-level field on one endpoint, because then clients cannot write one parser.

saying these in an interview costs you the question

  • Thinks a documented body format is enough without a shared call site
  • Builds the error payload inline in each handler with a literal object
  • Lets the thrown exception's own text become the client-facing message
  • Assumes only handler-thrown failures need to match the contract
  • Adds a new top-level field for one endpoint instead of extending the envelope
  • Treats the factory as the thing that decides which failure occurred
open as a page

Your handlers all return one failure body, yet an unmatched route or an unparsable request body returns a different shape. Why, and what fixes it?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Those failures are raised before handler invocation, so no handler code runs and the framework renders its own built-in body. Fix it by attaching the framework's central failure hook to the same factory, wrapping pre-handler failures like thrown ones.

open as a page

When the shared failure factory is handed a thrown exception, what should it put in the client-facing message field?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Text chosen from the mapped error code, not the exception's own message. The factory should default to a generic message for anything unrecognised, and send the exception text and stack to the log record keyed by the same correlation id.

open as a page

Should a shared failure-body factory take the correlation id as a parameter or read it from request-scoped context?

level: middleimportance: should knowfreq 44%

basics

~20 s

Read it from request-scoped context. A parameter works but puts the burden back on every call site, which is the drift the factory exists to remove. The factory must also define what it does when that context is empty.

open as a page

How do you keep one internal failure contract when two surfaces of the same service must serialize errors in different wire formats?

level: principalimportance: should knowfreq 34%

basics

~20 s

Split the failure value from its rendering. One factory produces a format-agnostic failure value; a renderer chosen per surface serializes it into that surface's standard. Handlers raise failures and never learn which surface they are serving.

open as a page