Engineering Notes21 October 20268 min read

Make API Errors a Contract, Not a Surprise

An HTTP status tells a client the class of failure. The body should name the problem, the field, and whether a retry can help.

BarathProduct Engineer
FIGURE 1.0
EDITORIAL REFERENCE
Source code on a laptop during an engineering review
CONTEXT:Source code on a laptop during an engineering review
REFERENCE:Luca Bravo
ON THIS PAGE (8 SECTIONS)
EXECUTIVE BRIEFING · KEY TAKEAWAYS
  • ✓Use the HTTP status for the class of failure. Put the specific reason in a stable, machine-readable body.
  • ✓RFC 9457 defines a problem-details document so every API does not invent a new error shape.
  • ✓Tell the client whether a retry is useful. A validation error retried unchanged will fail again.
  • ✓Do not put stack traces, SQL, or secrets in the body a caller can read.

A client calls your API, receives 500, and the body says "Something went wrong." The caller cannot tell a bad input from a dependency that timed out, so it either retries forever or shows the user a dead end. The status code did half a job. The body did none.

Treat the error as a contract. The status classifies the failure for intermediaries and generic clients. The body names the problem in a field the caller can branch on. RFC 9457, which obsoletes RFC 7807, defines a problem-details document for exactly this: a type, a title, and room for API-specific fields, carried as application/problem+json. You do not have to adopt that media type on day one. You do have to stop changing the shape of errors between releases.

What the caller is allowed to assume

StatusMeaning for the callerRetry the same request?
400 or 422The request is invalid as sentNo, until the caller changes it
401 or 403The caller is unauthenticated or not allowedNo, unless credentials or permissions change
404This identifier is not visible to the callerNo, unless the resource is created later
409The request conflicts with current stateOnly after the caller reads the new state
429The caller is over a limitYes, after the indicated wait, if you send one
5xxThe server or a dependency failedYes, if the operation is safe to repeat

A 409 that hides which version conflicted is still a surprise. Include the field or the version the client must reconcile. A 422 that says "invalid" without the field name forces the client to guess. Name the field.

Keep the type stable

The human sentence can change. The problem type should not. A client that switches on the string "Email already registered" will break when you edit the copy. A client that switches on a type such as email-taken will not. Log the internal exception, with a correlation id, on the server. Return the correlation id in the problem document so support can find the log. Do not return the exception text.

Model failures are a different contract. A language model can return HTTP 200 and still be unusable. That case is covered in AI Failure Contracts. This article is about the HTTP API around it: when the model call fails as a dependency, the API still owes the client a status and a type, not a stack trace.

Illustrative scenario

This scenario is illustrative. A billing API rejects a charge because the account is past a limit the client could not see. The response is 403, type account-limit, and a field saying which limit. The client shows that limit. It does not retry the identical charge. A timeout from the card network is 503, type dependency-timeout, and the client retries only because the charge endpoint requires an idempotency key. Without that key, a retry would be a second charge, which is the subject of Retry a Call Only When the Call Is Safe.

Checklist

  • Every error response has a status and a stable type.
  • Field-level failures name the field.
  • The body says whether the same request may be retried.
  • Internal exceptions stay in logs, keyed by a correlation id the client can quote.
  • Secrets, queries, and file paths are not in the response.
  • A contract test fails the build if a documented type disappears.

Questions that usually come next

Is a single 400 for every mistake enough?

It is enough for a prototype. It is not enough once more than one client exists. Callers need to tell a bad field from a conflict from a limit. Use the status for the class and a type for the specific case.

Should browsers and API clients share one error page?

People can read HTML. Programs need fields. If the same route serves both, negotiate the representation. Do not make the program parse a sentence you wrote for a person.

Direct answers

Is a single 400 for every mistake enough?

Only in a prototype. Callers need the class of failure from the status and the specific case from a stable type.

Should browsers and API clients share one error page?

People can read HTML. Programs need fields. Negotiate the representation rather than forcing a client to parse prose.

ACADEMIC & INDUSTRY CITATIONS

References & Prior Art

REF 01Internet standard

RFC 9457: Problem Details for HTTP APIs

M. Nottingham, E. Wilde, S. Dalal • IETF (2023)

Source
VERIFIED CONTRIBUTOR

Barath

Product Engineer

Barath builds production internal tools, double-entry financial ledgers, audit trail integrity, and supervised engineering workflows at Vasiva.

CURATED ARCHIVE

Continue Reading

Contextually relevant engineering logs and production architecture notes.

SYSTEM ADVISORY

Have an AI workflow or complex system worth exploring?

Work directly with the senior engineers who designed and shipped these systems. Zero vendor lock-in, deterministic architectures, and first-party audit integrity.

Evaluate the opportunity