An API is easy to describe on the happy path. A client sends a request, receives data, and renders the next useful screen. The product becomes much harder to use when the request is invalid, a quota is exhausted, an action conflicts with newer data, or an upstream service is unavailable.
At that point, the error response is no longer an implementation detail. It is an interface between the product, its integrations, its support team, and the person trying to finish a task.
“Returns an error” is not a requirement. A useful requirement is: when an action cannot proceed, the client can tell what happened, whether it is safe to try again, and what the person can do next. That expectation needs product, engineering, and quality to agree on the contract before an incident turns it into an accidental one.
Give errors meaning that clients can rely on
An HTTP status code gives important broad semantics, but it is rarely enough to drive a good product experience. A 403 can mean a missing entitlement, an account hold, an organisation policy, or a temporary safety restriction. Treating all four as the same generic toast creates needless support work and leaves the customer without a next move.
The IETF's Problem Details for HTTP APIs standard exists for this gap. It defines a common, machine-readable format rather than asking every endpoint to invent a different error shape. Its core fields separate a stable problem identity (type), a short human summary (title), occurrence-specific explanation (detail), and an identifier for this particular occurrence (instance).
The important design choice is not the JSON property names. It is what remains stable. A client should branch on a documented problem type or deliberately defined field, never on a sentence written for a human. A title can be localised. A detail can become clearer over time. A contract identifier needs to keep its meaning.
For example, a product that prevents an invoice from being sent twice might return a documented invoice-already-sent type, the relevant HTTP status, and a safe link or identifier for the existing invoice. The interface can then take the customer to the original result instead of offering a blind retry.
Pair the protocol answer with the product answer
Correct status codes matter because clients, gateways, and monitoring tools use them. They are not a substitute for the action a person needs to take.
Define each consequential error with four questions:
- What happened? Use a stable type and the HTTP semantics that best fit the condition.
- What can the client do safely? Correct input, refresh data, ask for access, wait, retry, or show the existing result are different outcomes.
- What should the person see? Give a concise explanation in product language, with a useful next action where one exists.
- What should support and operations see? Include an opaque occurrence or request reference that can be traced internally without leaking system internals.
The HTTP Semantics standard distinguishes response status from the representation sent with it. Keep both deliberate. A 409 Conflict can tell general HTTP software that the request conflicts with the current state, while the structured response tells the product whether to reload an edited record, show a competing reservation, or guide a customer to the result already created.
This also makes error copy more honest. “Something went wrong” is acceptable only when the product genuinely has no safer statement. If the system knows a submitted value is invalid, a payment needs another method, or a requested action is already complete, it can say so without exposing a stack trace.
Make retry a contract, not a reflex
Retry is among the most consequential error actions. It is helpful when a request did not reach the service or when a transient dependency is likely to recover. It is harmful when the original request may already have changed the world.
Before putting a retry button in a client, decide which of these it means:
- repeat a request that is safe to repeat;
- check the status of an earlier request with a stable reference;
- resume work that the server has recorded; or
- start a new action, clearly labelled as such.
Those are different contracts. The HTTP standard calls a method idempotent when repeating an identical request has the same intended effect as making it once. That property helps, but it does not automatically make every business operation safe to replay. A payment, a message, or a provisioning request may need a request key and a durable result lookup even when its transport call times out.
Build the server and the interface around the business outcome. A customer should not have to learn whether an ambiguous timeout happened before or after a database commit. When the outcome cannot be confirmed immediately, acknowledge the uncertainty, preserve the original intent, and give a reliable place to check it.
Keep diagnostic detail useful and safe
Errors need to be useful to three audiences: the client, the person using the product, and the team diagnosing the service. Giving all three audiences the same raw payload is usually a mistake.
Use structured, documented fields for data a client needs to act on: a validation pointer, a current version, a retry delay, a limit, or a link to a resolvable resource. Keep sensitive implementation detail—query text, internal hostnames, stack traces, credentials, and policy logic—on the server side, tied to an opaque correlation identifier.
RFC 9457 makes this boundary explicit: problem details are an HTTP interface, not a debugging tool, and they should not expose implementation internals that help an attacker. The same response can therefore guide a customer while letting a support engineer find the corresponding event through a reference.
Accessibility belongs in this work too. If a web client turns a validation response into an inline error, make the error identifiable in text and associated with the relevant input. The WCAG error-identification guidance is a useful baseline: people need to know which error occurred, not merely see that something is coloured red.
Test the contract at the boundary
Error handling often fails during change because only successful examples are treated as the API. Give important error types the same release discipline as successful responses:
- contract-test the status, content type, stable type, and action fields;
- check that clients do not parse human-readable details;
- exercise slow, duplicate, conflict, unauthorised, and unknown-outcome paths;
- verify the product copy and accessible announcement alongside the raw response; and
- monitor error types and recovery outcomes, not just total 4xx and 5xx counts.
This creates better signals. A rise in validation errors can reveal a confusing form. A spike in one conflict type can expose stale client data. A growing number of unknown outcomes can identify a reliability problem that successful request counts hide.
A dependable API helps people recover
An error response is where an API explains its boundaries. When its semantics are stable, its recovery path is explicit, and its diagnostic context is safe, clients can guide people forward instead of guessing.
The result is more than tidier JSON. It is a product that stays understandable when the path is not straight.
If your next product needs design, engineering, and quality to make those decisions together, bring BugSquad the challenge.

