REST error responses in 2026: RFC 9457 Problem Details vs the envelope you invented
Error responses are the least designed and most consumed part of most APIs. Teams spend weeks on the happy-path schemas and twenty minutes on the error body, which is what every client's retry logic, support tooling, and
Error responses are the least designed and most consumed part of most APIs. Teams spend weeks on the happy-path schemas and twenty minutes on the error body, which is what every client's retry logic, support tooling, and user-facing message depends on. The result is a dozen services with a dozen envelopes: {error: "msg"}, {code, message}, {errors: [...]}, {status, error: {message}}. In 2026 there is a mature standard — RFC 9457 Problem Details for HTTP APIs — and the case for adopting it is stronger than ever, especially once agents start calling your API.
What Problem Details looks like
The media type is application/problem+json, and the body has well-defined fields:
{
"type": "https://api.example.com/errors/plan-limit-reached",
"title": "Plan limit reached",
"status": 403,
"detail": "The free plan allows 1 active project. Archive a project or upgrade to Pro.",
"instance": "/v1/projects",
"errors": [
{
"detail": "Project count (1) exceeds plan limit (1).",
"pointer": "#/data/active_projects"
}
]
}
The five core fields:
-
type: a URI identifying the problem. Resolvable to human documentation is ideal, but even an opaque stable URN works as a machine key. -
title: a short human-readable summary, stable for thetype. -
status: the HTTP status repeated in the body, for clients that only see the body (logs, intermediaries). -
detail: a human-readable explanation specific to this occurrence. -
instance: the specific request URI or correlation reference.
Extensions are explicitly allowed — errors above is an extension for field-level validation detail. This is the standard's best design decision: it gives you a common spine without forbidding domain data.
Why a standard beats a bespoke envelope
- Generic tooling understands it. Gateways, API clients, and agent frameworks increasingly render Problem Details natively; a bespoke format always needs custom parsing.
-
The distinction between type and detail forces discipline.
typeis the stable machine code clients branch on;detailis the occurrence-specific sentence humans read. Conflating them — one free-textmessagefield used for both — is the most common design error and the source of brittle string-matching in clients. - HTTP status stays authoritative. The envelope reinforces rather than competes with the status code, so proxies and caches behave.
-
AI agents handle it without guessing. When an MCP tool call fails, an agent reads
typeanddetailand can correct the request (fix a field, request a different scope) rather than asking the user what a vendor-specific blob means.
Documenting it in OpenAPI 3.2
Define the problem schema once in components and reference it from every error response:
components:
responses:
BadRequest:
description: Malformed or invalid request.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
schemas:
Problem:
type: object
description: RFC 9457 Problem Details.
required: [type, title, status]
properties:
type:
type: string
format: uri
description: Stable identifier for the problem kind.
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
errors:
type: array
items:
type: object
properties:
detail: { type: string }
pointer: { type: string, description: 'JSON Pointer to the offending field.' }
parameter: { type: string }
Then each operation lists the statuses it actually returns — not a generic "500 for everything." The documented error set is part of the contract: a 409 Conflict with type: project-name-taken tells the client to surface a rename prompt; that flow cannot be built against an undocumented 400.
The status code conventions worth keeping
Problem Details does not replace HTTP semantics; it sharpens them:
| Status | Meaning the client needs | Typical type |
|---|---|---|
| 400 | Malformed request; do not retry unchanged | validation-failed |
| 401 | Missing/invalid authentication | unauthorized |
| 403 | Authenticated but not permitted | plan-limit-reached |
| 404 | No such resource, or hidden for security | not-found |
| 409 | Conflict with current state | name-taken, version-conflict |
| 422 | Well-formed request with semantic errors | semantic-validation |
| 429 | Rate limited; honor Retry-After | rate-limited |
| 5xx | Server fault; retry with backoff | internal-error |
Two conventions matter for agents and automation: include a stable, documented type for every branch a client might take (retry, prompt, fail), and on 429/503 include Retry-After as a header — the spec should document that the client must honor it.
Migration without a big-bang
Existing APIs usually cannot replace their envelope overnight. A low-risk path:
- Add
application/problem+jsonas an additional error media type, negotiated viaAcceptor enabled per API version; keep the legacy format for old clients. - Standardize internally first: new services emit Problem Details; a gateway adapter can translate legacy envelopes into it for generic tooling.
- Document both during the deprecation window, with the legacy response marked deprecated in the OpenAPI document and a sunset note.
- Align SDK error classes on
typerather than regex-matching messages.
What not to do
-
Do not put stack traces or internal service names in
detail. It leaks topology and confuses users; log those server-side and return a correlation id ininstance. -
Do not vary
titleper occurrence. It belongs to the type; per-occurrence text goes indetail. - Do not return 200 with an error body. A surprising amount of legacy API behavior does this, and it makes agents and caches unrecoverably wrong.
- Do not forget validation arrays. A form with five bad fields needs five entries; a single top-level error forces five round trips.
Designing the error model in the spec — and generating mocks that return realistic problems — lets frontend and agent developers build against failure on day one instead of discovering it in production. In Powerduck the problem schema lives in components like any other, scenario tests assert on both status and type, and mocks return the documented errors so failure flows are testable before the backend exists. The demo shows the workflow on a sample spec.
What to read next: a 200 is not done — run the business scenario makes the case for testing failure branches, and detect breaking API changes in CI catches the moment an error contract changes.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.