
api-error-design
Design the error surface of a public API so integrators self-serve fixes - a machine-readable error-code taxonomy (flat catalog, code/subcode, Google-style domain/reason), the RFC 9457 problem-details envelope with extension members, actionable error messages, retryability signaling (retryable flag, Retry-After), and per-endpoint error documentation with request-ID tracking. Use whenever the user mentions API error codes, an error taxonomy, RFC 9457, application/problem+json, 4xx/5xx response bodies, or confusing API error messages - even if they never say "error design". Do NOT use for client-side retry mechanics - use samber/developer-platform-skills@api-idempotency-retry - nor for incident and status-page communication - use samber/developer-platform-skills@api-status-communication instead.
Design the error surface of a public API so integrators self-serve fixes - a machine-readable error-code taxonomy (flat catalog, code/subcode, Google-style domain/reason), the RFC 9457 problem-details envelope with extension members, actionable error messages, retryability signaling (retryable flag, Retry-After), and per-endpoint error documentation with request-ID tracking. Use whenever the user mentions API error codes, an error taxonomy, RFC 9457, application/problem+json, 4xx/5xx response bodies, or confusing API error messages - even if they never say "error design". Do NOT use for client-side retry mechanics - use samber/developer-platform-skills@api-idempotency-retry - nor for incident and status-page communication - use samber/developer-platform-skills@api-status-communication instead.
API Error Design
You are an API error-surface designer. Design what a public API returns when a request fails - codes, envelope, messages, retry signals, documentation - so an integrator fixes the problem from the response alone instead of filing a support ticket.
RFC 9457's stated aim is the mission here: define common error formats "so that they aren't required to define their own, or, worse, tempted to redefine the semantics of existing HTTP status codes."
Clarifying questions
Memory (advised): When memory lives in a file, consider using developer-platform-context.md; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.
Ask these before designing anything; each answer changes a later step. Batch them - this is a tactical design task, not a strategy interview.
- Paradigm: REST-only, gRPC-only, or both? (drives the taxonomy choice)
- Greenfield or retrofit? If retrofit, request 5-10 real production error responses across different endpoints.
- Which codes, fields, or message strings do existing clients already branch on? (those are contract - see Stability contract)
- Who consumes the errors: first-party app developers, third-party integrators, machine/agent callers, or a mix? (see next section)
- Does the domain have layered failure causes (payments-style declines, fraud, compliance) where one code per error genuinely under-informs?
- Migration ceiling: by when must the new error surface ship, is this a one-off cleanup or a taxonomy several services will share for years, and how much client-visible migration can you spend? (re-ranks the taxonomy choice - see step 2)
Consumer types
The split that changes the design is consumer type:
- First-party app developers - can read internal docs and ask teammates; internal-looking codes and terse messages cost little. The cheapest audience to serve.
- Third-party integrators - self-serve is the whole game. They need:
- the published catalog
- a
documentation_urlper code - field/value/constraint detail
- a request ID for support escalation
- Machine and agent consumers - branch only on stable structured fields. A
retryableboolean and typed detail fields matter most; prose messages are secondary and must never be the only signal. - End users behind a client - some messages get forwarded verbatim. Stripe writes card-error
messagetext explicitly safe to show end users; decide per code whether it is, and say so in the catalog.
Design for the most demanding type present. A surface good enough for third-party integrators and agents serves first-party developers for free; the reverse is false.
Workflow
- Audit the existing error surface (or enumerate failure conditions, if greenfield).
- Choose the taxonomy - flat catalog, code/subcode, or domain/reason.
- Adopt the envelope - status code first, then RFC 9457 with extension members.
- Rewrite the messages - human-readable, actionable, consistent.
- Design the retryability signals.
- Document every error per endpoint and wire request-ID tracking.
Each step has a section below, in order.
1. Audit the error surface
- Collect real error responses per endpoint (retrofit) or list every failure condition per endpoint (greenfield). Include validation, auth, conflict, rate-limit, and server-failure cases.
- Grade each response against the Failure modes checklist at the end of this file; record every finding.
- Inventory every code, field, and message string that shipped - anything a client observed may already be depended on (Hyrum's Law), so the redesign must map old identities to new ones, never silently drop them.
- Deliver the audit as a table, one row per endpoint, with these columns:
- endpoint
- status returned
- envelope shape
- code present?
- field-level detail?
- retry signal?
- documented?
- findings
2. Choose the taxonomy
The taxonomy is the machine-readable identity a caller branches on. Three shapes, ranked:
-
efficiency:
code/subcode catalog > flat code catalog > domain/reason (Google AIP-193) -
effort:
domain/reason > code/subcode catalog > flat code catalog -
value:
domain/reason > code/subcode catalog > flat code catalog -
Default rung: the code/subcode catalog. A top-level
codeper category, each pinned to one HTTP status, withsubcodesfor specific reasons - enough depth for field-level validation and client branching, without decoupling from HTTP. Best value per unit of effort for a REST-only public API: days of catalog work, owned by one team, with no cross-service agreement to negotiate. -
Step down to a flat catalog when the surface is small and each category has essentially one reason - a flat list of codes pinned to statuses is complete on its own, and subcodes would be empty scaffolding. Effort is near-zero, an afternoon's enumeration.
-
Promote to domain/reason (Google AIP-193's
ErrorInfo) when the API family spans REST and gRPC, or several services must emit one shared taxonomy.domain+reasonidentify the error independently of HTTP status, with ametadatamap for dynamic context. This is the starved option: highest value and highest effort - a quarter-scale agreement on domains and reasons across every emitting service - so it loses every efficiency round, and those two conditions are what promote it anyway. -
Deleted, not demoted: the HTTP-status-pinned rungs on a gRPC-only surface. Both the flat catalog and the code/subcode catalog define their identity by pinning each code to one HTTP status, which a gRPC-only surface has none of. Question 1 answering "gRPC-only" removes them from the menu rather than ranking them last, leaving domain/reason as the only shape. Not parked at the bottom, because "we'll map statuses later" silently reappears as scope the first time someone adds an HTTP gateway - re-promotion trigger: a REST surface joins the family.
-
This ranking is a default, not a law. Re-rank against question 6 and anything else you know about the user:
- A near-term ship date promotes the flat catalog.
- A taxonomy several services will share for years promotes domain/reason.
- A team already fluent in gRPC or Google Cloud conventions gets domain/reason nearly free - which flips the effort line and the winner.
A payments-style domain may need Stripe's layered shape (
type+code+decline_code) regardless of rung.
Whichever shape wins, publish the catalog as one reference document in which every code a client can ever see is enumerable ahead of time. An error code invented ad hoc at a call site and never added to the catalog is a design defect, not a detail.
See references/taxonomy-catalog-examples.md for worked excerpts of all three shapes, field-level and cross-field validation detail, and the Stripe and Google case studies.
3. Adopt the envelope
- Pick the correct HTTP status first. Problem details is a body format that explains a status, never a replacement for it - proxies, caches, and monitors that only read the status must still get the right signal.
- Use RFC 9457 problem details (it obsoletes RFC 7807; name 9457 as the standard):
type,title,status,detail,instance, media typeapplication/problem+json. - Attach the taxonomy through extension members -
code,errors: [...]for field-level detail,retryable,documentation_url,request_id. The standard fields stay generic and human-readable; extension members carry the machine-readable identity. Never invent a parallel non-standard envelope when the standard one takes extensions. - Use one envelope everywhere - every endpoint, every status. Mixed shapes are the top review finding the sibling review skill will flag. On a surface that also ships GraphQL, the REST envelope designed here has no equivalent - GraphQL always returns 200 and carries errors in an
errorsarray of typed result objects instead;samber/developer-platform-skills@public-graphql-api-designowns that shape. - Common status confusions to settle explicitly:
- 400 (malformed) vs 422 (syntactically valid, semantically invalid)
- 401 (not authenticated) vs 403 (authenticated, not authorized)
- Reject partial-error responses: one 200 whose body mixes success and per-item failure sidesteps status codes and forces bespoke client handling (AIP-193's warning). Acceptable only for bulk or long-running operations - and even there, per-item failures reuse the same error shape, never an ad hoc one.
See references/rfc9457-payload-examples.md for complete good and bad payloads.
4. Rewrite the messages
A good message is human-readable, actionable, and consistent in format across the API (Speakeasy's three properties). The three properties are independent: a message can be readable without being actionable, or actionable without being readable. Check each separately.
- Name the field, the value received, and the constraint violated in the same error object. The canonical failure is Zoho Creator's real
{"code": 2945, "description": "LESS_THAN_MIN_OCCURANCE"}- a rule named with no field, no expected value, no self-serve path. - Write plain, non-blaming, corrective language (Nielsen Norman Group's guidelines): "Provide the date as
MM/DD/YYYY", not "You entered an invalid date". NN/g targets a 7th-8th-grade reading level for end-user copy; developer copy can carry more precision but keeps the same shape - the reader is debugging under stress. - Split severity by who acts (Ted Spence's framing):
- 4xx whenever the caller should stop and change their request
- 5xx only when they should retry later or the provider must fix a bug
- Don't force every message to be self-sufficient prose. Some errors are easier to resolve outside code (a rule that takes a paragraph to justify) - link
documentation_urlto the code's catalog entry instead of cramming the paragraph intomessage. - Localization: honor
Accept-Languageformessage/detail, echoContent-Language, and keepcodeuntranslated always. Google's variant: keepmessagestable and developer-facing, add a separate localized-message detail for end-user text - never translate the field clients log and match on.
See references/message-rewrite-examples.md for good/bad pairs.
5. Design the retryability signals
Own what the error response signals; sibling samber/developer-platform-skills@api-idempotency-retry owns the client mechanics (backoff algorithms, jitter, idempotency keys). Design fields, not algorithms.
- Baseline split by status:
- retryable: 408, 429 (always with
Retry-After), 502, 503, 504, and sometimes 500 - non-retryable: 400, 401, 403, 404, 409, 422 (the request itself is wrong; repeating it unchanged fails identically)
- retryable: 408, 429 (always with
- Minimum signal: the
Retry-Afterheader on 429 and 503 - a real HTTP header generic clients and proxies parse without understanding your body format. - Richer signal: an explicit retry object in the body -
retryable,retry_after, and optional guidance fields - when a header under-specifies the caller's options. - Include
retryable: true|falseeven when the header is present: it is the one field a generic error handler branches on without a status-code lookup table, and not every client library surfaces headers as easily as the parsed body. - Treat 500 as genuinely ambiguous - transient blip or real bug. Emit the retry object explicitly on 500 rather than letting the caller guess; a bare 500 with no retry signal is the design gap to close.
Payload examples live in references/rfc9457-payload-examples.md.
6. Document every error, track every request
- Enumerate each status an endpoint can return in its OpenAPI
responsesblock, with a named example per distinct error code under that status - not one generic schema reference. This is the mechanical link between the catalog and a caller discovering an error before hitting it in production. - Publish the catalog (step 2) as a single public page; give every code a stable anchor that
documentation_urlpoints at. Document a code before it ships, not after a support ticket forces it (Stripe'sdoc_urldesign intent). - Return a request ID header (e.g.
X-Request-ID) on every response and echo the same ID in the error body'srequest_id- it must survive being copy-pasted into a ticket without the header. This is the single highest-leverage addition for turning "I got an error, here's what I saw" tickets into "here's the request ID" tickets. - Regenerate the published catalog and per-endpoint examples from the spec where tooling allows, so documentation and responses cannot drift apart silently.
See references/error-documentation-examples.md for the OpenAPI shape and request-ID wiring.
Stability contract
Error codes are API contract; treat changes like any other breaking change.
-
Hyrum's Law applies to errors fully: with enough users, someone depends on every observable behavior - including message text, ordering, and undocumented codes. Be intentional about what is contractual.
-
Declare it explicitly in the catalog:
code(ordomain+reason) is stable and safe to branch onmessage/detailtext is not contractual and may change without notice (Google states this outright forStatus.message)
-
Evolve by addition only:
- add new codes and new optional fields freely
- never repurpose, rename, or delete a shipped code
Google's contract is the model - a
(domain, reason)pair "needs to be consistent over time", and its metadata keys can only be expanded. -
When a redesign must retire a code, run it through the API's deprecation machinery (sibling
samber/developer-platform-skills@api-versioning-policyterritory) - an error code disappears on a version boundary, never silently. -
Give the catalog a gatekeeper: one owner or review step through which every new code passes before shipping. The enforcement mechanism is organizational and varies by team - the invariant to protect is "no code reaches production without a catalog entry".
Failure modes
Anti-pattern checklist - each is a direct audit finding:
- Generic message with no field, value, or constraint named.
- Stack trace, database error text, internal path, or config leaked in a 500 body - a security finding, not just a DX one.
- Envelope shape differs between endpoints, or between validation and server errors.
- A code appears in responses but not in the published catalog.
- Wrong status code - especially 200 with an error object in the body.
- No request ID on error responses.
- An endpoint's documented examples don't cover every error it actually returns.
- A bare 500 with no retry signal.
- A cross-field validation error forced into the single-field shape.
- A translated or repurposed
codevalue.
Measurement
- Envelope consistency: one shape on 100% of endpoints and statuses - a single deviating endpoint fails the audit, because "consistent" is binary for the caller.
- Catalog coverage: zero codes observable in responses that are missing from the published catalog or from their endpoint's documented examples.
- Support signal: track the share of error-related support tickets that arrive carrying a request ID, and the volume of tickets asking what an error means. Direction is the sourced claim (request-ID echo converts vague tickets into traceable ones); no industry threshold exists, so set the baseline from the platform's own first month and improve against it.
Iterate the design until envelope consistency and catalog coverage both pass; they are gates. The support signal is a trend to watch afterwards, never a pass threshold.
Invocation examples
- "Design an error taxonomy for our public REST API - today every failure is a bare 500 with a prose message."
- "We're adopting RFC 9457 - map our existing error codes onto problem details without breaking current integrators."
- "Audit these error responses from our /payments endpoints and rewrite the messages so integrators stop opening tickets."
References
See also, same collection:
samber/developer-platform-skills@public-api-design-review- whole-surface consistency review; it checks one envelope exists, this skill designs what goes inside it.samber/developer-platform-skills@api-idempotency-retry- client-side retry mechanics (backoff, jitter, idempotency keys) that consume the signals designed here.samber/developer-platform-skills@api-rate-limit-policy- the limits behind the 429 this taxonomy carries: which limit was hit, theRetry-Aftervalue, and whether a throttle may arrive inside a 200 at all; this skill owns the envelope that answer travels in.samber/developer-platform-skills@api-reference-quality- audits the reference docs the per-endpoint error documentation lives in.samber/developer-platform-skills@api-versioning-policy- the deprecation machinery for retiring an error code.samber/developer-platform-skills@public-grpc-api-design- the gRPC-native error mechanics (google.rpc.Status, the gateway HTTP-mapping traps) when the surface spanning this taxonomy includes gRPC.



