How do you keep error response bodies consistent across every endpoint of an HTTP API — and across many services in one organisation — and what concretely breaks when they are inconsistent?
answer
- consistency = structural, not documented
- one global handler, no hand-rolled bodies
- shared library / service template
- gateway emits the same envelope for 502/504
- contract tests + spec lint; track coverage %
basics
~20 sEnforce one envelope in shared middleware, not by convention: a single global exception handler plus a shared library or gateway normalisation, with contract tests. Otherwise every client writes N parsers and error handling silently rots at the edges.
solid answer
~50 sConsistency has to be *structural*, not documented. Practical layers: 1. **One global exception handler per service** so no endpoint hand-rolls a body and no framework default page escapes. 2. **A shared library or template** owning the envelope, so a new service inherits it rather than reinventing it. 3. **Gateway normalisation** for what the application never sees — proxy 502/504, gateway auth rejections, body-size limits. These are the most commonly inconsistent responses in real systems. 4. **Contract tests and spec linting** that assert every documented error response uses the envelope and media type. What breaks otherwise: clients write a parser per endpoint and end up with defensive code that gives up and shows "something went wrong"; error-rate dashboards can't aggregate because the code field has three different names; a new endpoint quietly returns a different shape and a client crashes on a null field. Version the envelope centrally so a change rolls out once, not per team.
go deeper
Know that every endpoint should return the same error shape so a client writes one parser.
Name the mechanism — a single global exception handler — and describe the client-side pain of multiple shapes.
Add shared libraries, gateway normalisation for 502/504 and size limits, and contract tests that assert the shape.
Frame it as governance: who owns the envelope, how it versions, how new services inherit it by default, and how migration progress is measured across the estate.
## Why inconsistency happens Nobody designs three error shapes on purpose. They accumulate: the original endpoints return `{error: "..."}`; a later team adopts `{code, message}`; a third adopts `application/problem+json`; the framework's default handler produces a fourth shape for unhandled paths; and the gateway emits a fifth (often HTML) for 502s and request-size rejections. Each is locally reasonable and the aggregate is unusable. ## Concrete damage - **Client complexity.** A client cannot write one error parser. It ends up with try/guess logic — look for `code`, else `error`, else `detail`, else give up — and the give-up branch renders a useless generic message, so real, actionable failures become invisible to users. - **Broken observability.** Dashboards and alerts want to group by error code. If the field is `code` here, `errorCode` there, and absent in gateway responses, aggregation silently under-counts, and the failures that don't parse are exactly the ones from the least-maintained paths. - **Silent breakage on growth.** A new endpoint that returns a different shape doesn't fail any test — it fails at the client, in production, often as a null-dereference rather than a clean error path. - **Support cost.** No correlation id in some shapes means some tickets are simply untraceable. - **Retry logic degradation.** If retryability is expressed inconsistently, clients fall back to retrying on status alone, retrying things that will never succeed. ## Enforcement layers **Application.** One global exception handler that maps known exceptions to codes and everything else to a generic 500 in the standard envelope. The rule is: no controller constructs an error body by hand. This also closes the trace-leak hole, since nothing falls through to a framework default page. **Organisation.** Put the envelope in a shared library or service template so adoption is the path of least resistance. Documentation alone does not survive team growth. Adopting a standard media type — `application/problem+json` — helps here because it removes the design argument and gives generated clients something to key on. **Edge.** The responses your application never generates are the ones that break clients hardest. Configure the gateway or reverse proxy to emit the same JSON envelope for its own errors: upstream timeouts, unavailable backends, oversized bodies, rejected TLS or auth at the edge. Many teams discover these only when a client reports "the error page is HTML". **Tests.** Assert the shape, not just the status: a shared test helper that every service uses to validate error responses, plus API-spec linting that rejects an operation documenting an error response with a different schema. If you generate clients from the spec, this becomes near-automatic. ## What consistency should *not* mean Uniform shape does not mean uniform content. The envelope is fixed; the code vocabulary is per-domain, and structured extras (which field failed, which limit was hit) live in a well-known extension slot. Over-standardising the *values* leads to a generic code set that discriminates nothing. ## Migrating an inconsistent estate Don't attempt a big-bang rewrite. Practical sequence: pick the target envelope; make new endpoints emit it; add the gateway normalisation (largest win per unit of effort, since it covers all services at once); then convert existing endpoints, publishing both shapes only where a client genuinely cannot be updated. Track coverage as a number, because "we standardised errors" is otherwise unfalsifiable.
- Your gateway returns HTML for upstream timeouts while services return JSON. Why is that worth fixing early?Those gateway responses arrive exactly when the system is already degraded, so clients hit the unparseable shape during incidents when correct handling matters most. A JSON parse failure typically routes into the client's generic catch-all, so the real failure mode is misreported. Configuring the gateway to emit the standard envelope fixes it for every service behind it at once, which is the cheapest large win available.
- How do you evolve the shared error envelope once many clients depend on it?Treat additions as safe and removals or renames as breaking. Add new optional members and require clients to ignore unknown ones — a rule you state in the contract from day one. If a structural change is unavoidable, roll it out behind a content negotiation or a new media type rather than mutating the existing shape, and keep the ownership of that decision in one place rather than per team.
saying these in an interview costs you the question
- Believing a written style guide is sufficient enforcement without shared code or tests
- Overlooking gateway and proxy error responses, which clients hit precisely during incidents
- Standardising the error code values organisation-wide until they no longer discriminate anything
- Allowing individual controllers to construct ad-hoc error bodies alongside the global handler
- Attempting a big-bang migration instead of normalising at the edge first and converting incrementally