When to use
Use at the start of any HTTP API design, when adding a new error path
to an existing endpoint, when reviewing an error-handling change, or
when authoring the client-side error parser.
Apply the four-field envelope below to every error response, including
auth failures, validation failures, downstream-dependency failures, and
internal errors. Consistency across the surface matters more than the
specifics of any single endpoint.
If you are inheriting an existing API with a different shape, this
skill gives you the target. Migrate endpoint by endpoint; do not mix
shapes inside a single endpoint.
Examples
The canonical error envelope:
{
"error": {
"code": "user_not_found",
"message": "No user exists with the given ID.",
"details": {
"user_id": "u_12345",
"trace_id": "01J7X9K2E5RQZT8M3P6VFYBNC0"
},
"retryable": false
}
}
Four fields, no others:
- code — stable, machine-readable, snake_case. The client SDK
switches on this. Never localised, never changed without a major
version bump.
- message — human-readable, English. May be shown to the end user.
Localisation is the client's job.
- details — structured context. Field names, IDs, trace IDs.
Optional but always present as
{} if empty.
- retryable — boolean.
true for transient failures (503, 429,
upstream timeout). false for permanent failures (400, 401, 404,
422). The SDK uses this to decide whether to retry automatically.
For example, a 401 Unauthorized response:
{
"error": {
"code": "auth_token_expired",
"message": "The session token has expired.",
"details": { "expired_at": "2024-09-18T03:14:15Z" },
"retryable": false
}
}
For example, a 503 Service Unavailable response:
{
"error": {
"code": "upstream_timeout",
"message": "The downstream service did not respond in time.",
"details": {
"upstream": "billing-api",
"trace_id": "01J7X9K2E5RQZT8M3P6VFYBNC0"
},
"retryable": true
}
}
Pitfalls to avoid
- Do not leak stack traces, query text, or file paths in
message.
These are developer-facing diagnostics and they tell attackers where
the seams are. Move them to logs.
- Do not change
code values without a major version bump. Clients
pin on this string; renaming it silently breaks every deployed SDK.
- Do not invent a new error envelope for a new endpoint. Use the
same four fields every time, with
details carrying the endpoint-
specific context.
- Do not put the reason for a 4xx in the HTTP reason phrase alone.
Reason phrases are not parsed by clients; only the body is. The body
is the contract.
- Do not return 200 OK with an error body. It breaks every cache,
every proxy, every monitoring rule that assumes status code semantics.
Pick a 4xx or 5xx status that matches.
- Do not return a different shape for 5xx than for 4xx. Internal
errors deserve the same structured envelope so the SDK can switch on
code uniformly.
- Do not omit
retryable. The default behavior on the client side
is "do not retry", but that default is wrong for transient failures.
Make the server say so explicitly.
- Do not include
details from another tenant. Multi-tenant APIs
must scrub the details object to only fields the caller is authorised
to see.
1---2name: api-error-response-format3description: Use when designing or implementing an HTTP API's error response shape, when reviewing a PR that adds a new error path, or when a client SDK needs to consume errors. Triggers: "how should we return errors", "what's the JSON error schema", "this 500 leaked a stack trace", "the error structure is inconsistent". Do not use for logging-format questions, exception handling inside the service, or for non-HTTP protocols (gRPC, message queues).4---56## When to use78Use at the start of any HTTP API design, when adding a new error path9to an existing endpoint, when reviewing an error-handling change, or10when authoring the client-side error parser.1112Apply the four-field envelope below to every error response, including13auth failures, validation failures, downstream-dependency failures, and14internal errors. Consistency across the surface matters more than the15specifics of any single endpoint.1617If you are inheriting an existing API with a different shape, this18skill gives you the target. Migrate endpoint by endpoint; do not mix19shapes inside a single endpoint.2021## Examples2223The canonical error envelope:2425```json26{27 "error": {28 "code": "user_not_found",29 "message": "No user exists with the given ID.",30 "details": {31 "user_id": "u_12345",32 "trace_id": "01J7X9K2E5RQZT8M3P6VFYBNC0"33 },34 "retryable": false35 }36}37```3839Four fields, no others:4041- **code** — stable, machine-readable, snake_case. The client SDK42 switches on this. Never localised, never changed without a major43 version bump.44- **message** — human-readable, English. May be shown to the end user.45 Localisation is the client's job.46- **details** — structured context. Field names, IDs, trace IDs.47 Optional but always present as `{}` if empty.48- **retryable** — boolean. `true` for transient failures (503, 429,49 upstream timeout). `false` for permanent failures (400, 401, 404,50 422). The SDK uses this to decide whether to retry automatically.5152For example, a 401 Unauthorized response:5354```json55{56 "error": {57 "code": "auth_token_expired",58 "message": "The session token has expired.",59 "details": { "expired_at": "2024-09-18T03:14:15Z" },60 "retryable": false61 }62}63```6465For example, a 503 Service Unavailable response:6667```json68{69 "error": {70 "code": "upstream_timeout",71 "message": "The downstream service did not respond in time.",72 "details": {73 "upstream": "billing-api",74 "trace_id": "01J7X9K2E5RQZT8M3P6VFYBNC0"75 },76 "retryable": true77 }78}79```8081## Pitfalls to avoid8283- **Do not** leak stack traces, query text, or file paths in `message`.84 These are developer-facing diagnostics and they tell attackers where85 the seams are. Move them to logs.86- **Do not** change `code` values without a major version bump. Clients87 pin on this string; renaming it silently breaks every deployed SDK.88- **Do not** invent a new error envelope for a new endpoint. Use the89 same four fields every time, with `details` carrying the endpoint-90 specific context.91- **Do not** put the reason for a 4xx in the HTTP reason phrase alone.92 Reason phrases are not parsed by clients; only the body is. The body93 is the contract.94- **Do not** return 200 OK with an error body. It breaks every cache,95 every proxy, every monitoring rule that assumes status code semantics.96 Pick a 4xx or 5xx status that matches.97- **Do not** return a different shape for 5xx than for 4xx. Internal98 errors deserve the same structured envelope so the SDK can switch on99 `code` uniformly.100- **Do not** omit `retryable`. The default behavior on the client side101 is "do not retry", but that default is wrong for transient failures.102 Make the server say so explicitly.103- **Do not** include `details` from another tenant. Multi-tenant APIs104 must scrub the details object to only fields the caller is authorised105 to see.