API Error Models: Designing Errors That Integrators Can Actually Use
How RFC 9457 structures an HTTP error body, which parts a program can rely on, and how to change an error contract without stranding clients.
An error model is the contract for the response a client gets when the call does not succeed. Integrators use it to decide whether to retry, what to show a person, and which field to fix. A body that changes shape per endpoint, or a success status that hides a failure flag, forces every client to special-case the call. This note follows RFC 9457, which obsoletes RFC 7807 and defines the problem-details document. It does not claim that every public API uses that document.
Use the HTTP status for the class of outcome
The status line is what generic HTTP software understands. RFC 9457’s status member, when present, repeats the code the origin server generated. The specification says that member is advisory for consumers, and that generators must use the same code on the actual response so intermediaries that have never heard of problem details still behave. If a proxy can change the code, the member lets a later reader see what the origin sent. It is not a second, conflicting source of truth. Do not return 200 with status: 400 inside the body.
Problem details use the media type application/problem+json when serialized as JSON. There is also application/problem+xml. A client that branches on Content-Type can tell a problem document from a success body. A client that only checks for an error property cannot, once a success schema gains a field with that name.
The five members, and the default type
RFC 9457 defines five members.
| Member | Role in the specification | Practical constraint |
|---|---|---|
type |
A URI reference that identifies the problem type. Consumers must use it as the primary identifier. If absent, the value is about:blank. |
Give stable types to failures a client handles differently. Leave about:blank for “no more meaning than the status code.” |
title |
A short human-readable summary of the type. It should not change between occurrences, except for localization. | Do not put the order id or the bad field value in the title. Those vary per occurrence. |
status |
The HTTP status code generated for this occurrence. Advisory, and it must match the response. | Clients that only store the body still need it. Clients that can read the status line should not prefer it when they disagree. |
detail |
A human-readable explanation of this occurrence. It ought to help the client correct the problem. | People can read it. Programs should not parse it. The specification says so directly. |
instance |
A URI reference for this occurrence. It may be dereferenceable, or it may be an opaque id. | The specification recommends absolute URIs when possible, because a relative reference resolves against the base URI and can collide. |
The middle column follows RFC 9457. The right column is this desk's reading of how to apply it. No cell is a usage statistic.
When type is about:blank, the problem has no additional semantics beyond the HTTP status code. The title should be the recommended reason phrase for that code, such as “Not Found” for 404, though it may be localized. A missing type member means about:blank. A document that omits type and then puts a machine-specific meaning in detail has thrown that meaning away for any client that follows the specification.
Extension members are how a program receives structured facts: a list of field names, a code from your own stable set, a retry hint that is not already on the HTTP Retry-After header. Document each extension in the same schema as the rest of the error body. Request and response schemas next to the error schema should include that object, not only the 200 response.
The illustration is not a captured error.
{
"type": "https://api.client.test/problems/invalid-field",
"title": "A field is invalid",
"status": 400,
"detail": "quantity must be an integer greater than or equal to 1.",
"instance": "https://api.client.test/requests/4f2c",
"invalidFields": ["quantity"]
}
Illustration, not a captured message. invalidFields is an extension member in this example, not a member defined by RFC 9457.
One model, many types
Integrators can handle a new type they do not recognize if the five members still parse and the status code is honest. They cannot handle a new JSON shape per endpoint. Publish one problem schema. Add types when the handling differs: a validation failure, a conflict, an unavailable dependency. Do not add a type for every sentence you might want to show.
RFC 9457 also creates an IANA registry of problem types and clarifies reporting more than one problem. You do not have to register a private type URI to use the media type. You do have to keep your own type URIs stable. Changing a type URI is a contract change, even when the prose in title looks the same.
Validating the error body itself is worth doing in CI. A producer that validates only the success schema will ship a problem body that does not match the documented extensions, and clients will fall back to parsing detail.
Changing the error contract
Adding an error field without breaking existing clients follows the same rule as any other response field. A new extension member is compatible for clients that ignore unknown names. Renaming invalidFields, or moving a machine value into detail and deleting the extension, is a break for any client that read the old member.
A change of status code for an existing failure is a behavior change even when the body schema is untouched. Retry logic often branches on the status code. Recategorizing a failure can change that behavior for any client whose rule keys off the old code, so say so in the version notes. Keeping errors stable while a contract is migrated means the error model is part of the expand phase, not an afterthought once the success body has moved.
An error-model checklist
- Failures use an HTTP status that matches the outcome. The body does not contradict it.
- The media type is
application/problem+jsonwhen the body is a problem document. typeis stable for each distinct handling.about:blankis only “see the status code.”titleis stable per type.detailis specific to the occurrence and is not parsed.- Values programs need are extension members, documented in the schema.
instance, when present, identifies the occurrence and does not leak internal hostnames or secrets. That last constraint is editorial: the specification warns against confusing relative URIs and leaves the contents of the occurrence record to the server.- Unknown extensions are ignored by clients you ship, so a new member is not a break.
- The error schema is versioned with the rest of the contract.
What this note leaves open
RFC 9457 does not require you to use problem details. A private error schema can be honest if it is one schema, documented, and stable under the same compatibility rule. The failure mode to avoid is a different object on every route, with the only reliable field being a sentence. Integrators can use a contract. They cannot use a surprise.
Sources
- RFC 9457, Problem Details for HTTP APIs — IETF
- OpenAPI Specification 3.1.0, Schema Object — OpenAPI Initiative