On this page
A failed request is still part of your API contract. If one endpoint returns a string, another returns a framework exception, and a third responds with HTTP 200, every consumer must invent different recovery logic. A small, explicit error model makes integrations easier to implement and much easier to support.
Start with a shared error contract
Collect real failures before designing the response: malformed JSON, expired authentication, missing resources, conflicting updates, and unavailable dependencies. Paste sanitized examples into the JSON Formatter to compare their structure. Do not paste production tokens, customer addresses, or private stack traces into any debugging tool.
Choose Problem Details, the standard defined in RFC 9457, rather than inventing a deeply nested envelope. Its familiar fields let clients distinguish a general problem category from the circumstances of one failure. Send the media type application/problem+json when using this representation.
Write down which fields are required by your own contract. The standard allows flexibility; your clients benefit from fewer surprises. Require a stable type identifier, a short title, and a status matching the HTTP response. Treat human-readable detail as explanatory text, not as an identifier that applications parse to make decisions.
Map failures to meaningful HTTP statuses
Use HTTP semantics consistently across endpoints. A 400 response can describe malformed request syntax. A 401 means authentication is required or failed and requires an appropriate WWW-Authenticate challenge. A 403 means the request is understood but refused. A 404 means the resource is not found, or deliberately concealed under your authorization policy.
For a validly encoded request that violates field rules, 422 is often useful; choosing 400 instead is defensible if the whole API documents that convention. Use 409 for conflicts with current resource state, such as an already-claimed username. Reserve 500 for unexpected server failures rather than ordinary user mistakes.
Read MDN's status reference when choosing less common codes. Avoid selecting codes solely because a framework uses them by default.
Keep the transport status meaningful. A 200 response containing an error flag can be cached, logged, and counted as success by infrastructure. Conversely, returning 500 for invalid input encourages pointless retries and hides actual reliability failures in operational reports.
Define a useful problem document
A practical response separates machine-readable identity from text that may change. Use a URI you control as the problem type; it can lead to documentation explaining the issue and recovery steps. Keep occurrence identifiers opaque if they appear in public responses.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Cache-Control: no-store
{
"type": "https://api.example.com/problems/invalid-input",
"title": "Some fields are invalid",
"status": 422,
"detail": "Correct the highlighted fields and submit again.",
"instance": "/problems/occurrences/req_73f2",
"errors": [
{ "pointer": "/quantity", "code": "minimum", "message": "Use at least 1." }
]
}The example's errors array is an extension, not a field mandated by the standard. Define whether pointers follow JSON Pointer, how array indexes work, and whether one field can have several failures. This is validation metadata, not an invitation to expose your internal validation library's entire object.
Clients should branch on documented problem types and extension codes, not translated messages. For frontend consumers, the existing guide to asynchronous error states explains why recoverable validation failures deserve a different interaction from an unavailable service.
Centralize translation without hiding context
Create one boundary that translates known domain exceptions into public problems. Keep domain errors independent of HTTP so the same operation works from a job worker or command-line task. An inventory conflict can become HTTP 409 at the API boundary without teaching the inventory model about response headers.
Unexpected exceptions should receive a safe generic response. Record the full exception in restricted logs with an interaction identifier, but do not return database queries, environment variables, or filesystem paths. Apply redaction before logging request bodies; an error handler can accidentally become the largest repository of sensitive data in a system.
Avoid catching everything early and returning the same generic failure from every layer. That destroys the distinction between a dependency timeout and an application defect. Preserve causes internally while exposing a deliberately limited public vocabulary.
For upstream failures, decide whether the caller can act. A dependency outage may justify 503, while an invalid upstream response may justify 502. Only send retry guidance when retrying is actually appropriate and the operation's side effects are understood.
Test client behavior and contract stability
Build contract tests around representative failures, not just success responses. Verify status, content type, required fields, and absence of secrets. Include malformed JSON, unsupported content types, oversized payloads, authorization denial, business conflicts, and unexpected exceptions. Test failures raised before route handlers, because framework middleware often emits a different shape.
Keep a fixture for each public problem type. Review changes to identifiers and extension codes like other breaking API changes. Adding an optional diagnostic field may be compatible, but changing a field from string to object can break deployed clients even when the HTTP status stays identical.
Use OpenAPI to document shared problem schemas and endpoint-specific responses. A schema alone cannot describe every recovery rule, so include examples explaining whether a client should edit input, authenticate, wait, or contact support. Link this reference from your wider development documentation.
Measure errors by status and problem type, with bounded labels. Never use raw detail strings, customer identifiers, or occurrence URLs as metric labels. Track unknown failures separately so a newly introduced exception cannot quietly disappear inside a large bucket of expected validation errors.
Design a fallback for unrecognized failures
Even a well-documented API can return an unfamiliar problem type after a deployment. Clients should retain a generic recovery path: inspect the HTTP status, display a safe message, and preserve a support identifier when supplied. Unknown extensions should not make the entire response unreadable. Avoid displaying raw response bodies as a fallback because a proxy may return an HTML error page instead of your JSON contract.
For example, a client expecting an inventory conflict may receive a new account-restriction problem. It should not interpret every 409 as permission to retry the purchase automatically. Use known problem types for specialized actions and a conservative default for everything else.
Test that fallback with an unknown type, missing optional detail, and a non-JSON gateway response. Verify that the application neither crashes while parsing nor loses the user's entered order information. For writes with uncertain outcomes, offer a status check or a safe retry mechanism rather than asserting that nothing happened merely because an error was displayed.
Key takeaways
- Use a standard problem representation with stable identifiers and accurate HTTP statuses.
- Keep validation extensions documented and human messages separate from application decisions.
- Centralize translation while preserving private diagnostic context.
- Test middleware failures, security redaction, and client recovery as part of the public contract.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor