skip to content

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?

level: middleimportance: must knowfreq 68%

answer

  1. code = contract, message = prose
  2. status too coarse: many 409s
  3. never string-match the message
  4. request id for support tickets
  5. new codes ok, repurposed codes never

basics

~20 s

An 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 s

A 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
json
{
  "code": "billing.card_declined",
  "message": "The card was declined by the issuer.",
  "requestId": "01J8YQ2R4K7ZC3M9"
}

go deeper

for a junior

Know the three ingredients — code, message, request id — and that the status alone is too coarse to distinguish business failures.

for a middle

Explain why the code is contract and the message is not, and how that enables rewording and localization without breaking clients.

for a senior

Talk about code namespacing, forward compatibility for unknown codes, and documenting the code catalogue as part of the API spec.

for a principal

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

context