What should the body of an HTTP API error response contain, and why do teams separate a stable machine-readable error code from the human-readable message?
answer
- code = contract, message = prose
- status too coarse: many 409s
- never string-match the message
- request id for support tickets
- new codes ok, repurposed codes never
basics
~20 sAn error body should carry a stable machine-readable code, a human message, and a request id for support. Clients branch on the code; the message is prose that can be reworded or localized without breaking any caller.
solid answer
~50 sA good error body does three jobs. **Machine dispatch**: a stable, documented code such as `CARD_DECLINED` or `QUOTA_EXCEEDED` that a client can branch on. **Human diagnosis**: a message saying what went wrong, plus safe context (which resource, which limit). **Support correlation**: a request or trace id the caller can quote in a ticket. Code and message are split because their contracts differ. The code is part of the published API contract: once shipped its meaning is frozen and clients may switch on it. The message is prose you want to reword, translate, or sharpen at any time. If clients string-match messages, every copy edit becomes a breaking change and localization is impossible. The status code alone is not enough — many distinct business failures share `409` or `422`, so the body carries the discrimination the status cannot express. Keep the code namespaced and enumerable, and document every value.
code
json · 5 lines{
"code": "billing.card_declined",
"message": "The card was declined by the issuer.",
"requestId": "01J8YQ2R4K7ZC3M9"
}go deeper
Know the three ingredients — code, message, request id — and that the status alone is too coarse to distinguish business failures.
Explain why the code is contract and the message is not, and how that enables rewording and localization without breaking clients.
Talk about code namespacing, forward compatibility for unknown codes, and documenting the code catalogue as part of the API spec.
Frame it as a versioning and coupling decision across many services and client generations: what is frozen, how new codes roll out safely, and who owns the catalogue.
## Why the body exists at all An HTTP status code says what *class* of thing went wrong: client fault, server fault, conflict, unauthorized. It is a small fixed vocabulary, so dozens of genuinely different business failures collapse onto `400`, `409` or `422`. The response body is where the API says *which* failure this actually was. ## The three consumers 1. **Program logic.** The client needs to decide: retry, refresh a token, show a card-entry form, or surface a fatal error. That decision must be driven by a value that never changes meaning — a *stable error code*. 2. **A human debugging.** A developer reading logs or a support agent reading a screenshot needs a sentence in words: what failed, on which resource, and ideally what to do. 3. **Support and operations.** When the caller opens a ticket, someone must find the exact server-side event. That needs a **request id / correlation id** echoed in the body. ## Stable codes A stable code is a short symbolic token — `INSUFFICIENT_FUNDS`, `ORDER_ALREADY_SHIPPED`, `RATE_LIMITED`. Properties that make it useful: - **Enumerable and documented.** Clients can write exhaustive handling and detect unknown values. - **Frozen semantics.** You may add new codes (a client should treat unknown codes as "generic failure of this status class"), but you must not repurpose an existing one. - **Independent of wording.** Changing the message must never change the code, and vice versa. - **Namespaced when the surface is large**, e.g. `billing.card_declined`, so two teams don't collide on `INVALID`. A common mistake is to use a numeric internal code (`error: 4471`) with no dictionary. It is stable but unreadable, and every consumer needs your lookup table. ## Human messages Messages are the volatile half. Treat them as *not* part of the contract: state that explicitly in your API docs so clients don't parse them. That freedom is what lets you localize (`Accept-Language`), improve clarity after a support spike, or add detail. Two practical rules: never put an untrusted input value into a message without escaping considerations at the client, and never put internals (SQL, class names, file paths) there. ## Codes vs HTTP status They are layered, not redundant. The status drives generic infrastructure — proxies, browsers, HTTP client libraries, retry policies. The code drives your business logic. Return a correct status *and* a specific code: `409` + `ORDER_ALREADY_SHIPPED` is far better than `200` with `{"success": false}`, which defeats every generic tool in the stack. ## Shape Keep one envelope for the whole API — a single object with the code, the message, an id, and optional structured details. Whether you invent that envelope or adopt the standard `application/problem+json` document, the important part is that every endpoint returns the *same* shape, so a client writes one parser.
- What should a client do when it receives an error code it has never seen before?Fall back to handling by HTTP status class: treat unknown 4xx codes as a non-retryable client fault and surface the server-supplied message, and unknown 5xx as a possibly-transient server fault. Adding codes must therefore be a non-breaking change, which means clients must never assume the code set is closed. Log the unknown code so the team notices new failure modes.
- Should the error message be localized on the server or the client?Prefer the client: it knows the user's locale and can render richer, product-appropriate copy, keyed off the stable code. The server message then serves developers and can stay in English. If the server must localize, drive it from Accept-Language and still send the stable code so the client isn't dependent on the translated text.
saying these in an interview costs you the question
- Returning HTTP 200 with a success:false body, hiding failures from proxies, monitoring and HTTP clients
- Expecting clients to branch on the exact wording of the message
- Treating the HTTP status code alone as sufficient to distinguish business failures
- Reusing one generic code like INVALID_REQUEST for every 4xx so nothing is actionable
- Changing the meaning of an existing published code instead of adding a new one