Back to Explore
GuideVibeFix 编辑部Updated Oct 18, 2026

Error Code Design in Practice: Errors That AI Agents and Frontends Can Actually Handle

Errors are your most-called interface asset: RFC 9457 problem details, stable UPPER_SNAKE_CASE codes, an HTTP status decision table, i18n copy engineering, traceId propagation, frontend discriminated types, and agent-executable error contracts.

A real photo of a developer wearing headphones at a desk facing multiple screens showing code and terminals at night

Three boundaries first, so this guide doesn't collide with others on the site: this article does not cover error monitoring and alerting after errors happen — for Sentry/Datadog integration, SLOs and error budgets, see the Error Monitoring Guide; it also does not cover form-validation interaction details (where to place inline hints, when to validate live) — that belongs to the Form Validation UX Guide. This guide answers exactly one question: when your API or system returns an error, what should its "shape" look like? How do you name error codes so they don't rot in five years? What information does each of the five fields — type, title, status, detail, instance — carry? How far down may 5xx details be exposed? And how should frontend SDKs and AI agents build automatic degradation and user guidance on top of this contract?

Error responses are the most frequently invoked "interface asset" in any system — and usually the most casually designed. A typical vibe-project error looks like this:

HTTP/1.1 200 OK
{"success": false, "message": "Something went wrong"}

This response has four fatal flaws: the HTTP status is 200, so gateways, CDNs and browser caches all treat it as success; message is human prose in one language, so any copy rewrite silently breaks the frontend's if (message.includes('wrong')); there is no machine-readable stable identifier, so an AI agent can only feed the whole sentence to an LLM and guess; and there is no request trace identifier, so support has nothing to look up when a user reports it. Worse, this kind of error design compounds the moment you ship third-party SDKs, publish an OpenAPI spec, or let agents call your API automatically — every consumer has to re-guess your error semantics.

This guide gives you a complete system: error-code naming conventions (stable UPPER_SNAKE_CASE identifiers) + RFC 9457 Problem Details response shape + an HTTP status mapping decision table + i18n copy engineering + traceId propagation + frontend discriminated types + agent-actionable extension fields + error-code governance. Every section ships with code and configuration you can drop in directly — follow it and you can rebuild your project's error system into a "won't rot in five years" version in an afternoon.

1. Error-Code Naming: Stable Identifiers Decoupled from HTTP Status

The first principle of error-code design: an error code is a machine-readable contract identifier, not human copy. Once published, a code's meaning never changes — only add, never reuse, never silently redefine. That means avoiding anything volatile in names: no version numbers (V2_PLAN_LIMIT), no internal module abbreviations (BILL_MOD_004), no bare numbers (10023 explains nothing; Twilio-style numeric codes like 21211 only work at massive scale, paired with a documentation site per code).

The recommended convention is UPPER_SNAKE_CASE + domain prefix + specific cause:

// Good: self-explanatory, stable, greppable
PLAN_LIMIT_REACHED        // plan quota exhausted
PAYMENT_CARD_DECLINED     // card declined
AUTH_TOKEN_EXPIRED        // session expired
RESOURCE_NOT_FOUND        // resource doesn't exist
QUOTA_RATE_LIMITED        // rate limit hit

// Bad: meaning drifts, not self-explanatory
ERROR_400                 // coupled to a status code; rots the day the status changes
BIZ_10023                 // bare number, meaningless without docs
PLAN_LIMIT                // reached or queried? missing verb, ambiguous semantics

Three hard rules. First, decouple error codes from HTTP status codes. The status code expresses only transport semantics (401 unauthenticated, 404 not found, 429 rate-limited); the error code expresses business semantics (quota exhausted vs. trial expired). The same PLAN_LIMIT_REACHED might return 402/403 today and 429 after a future business change — frontends and agents match on the code, not the status, so business logic survives status changes. Second, never use 400 as a catch-all. Stuffing every unclassifiable error into 400 is where error systems start to rot — consumers can never distinguish "bad input" from "server bug", and retry strategies become impossible. The only legitimate catch-all is a 5xx INTERNAL_ERROR, and it must carry a traceId (see section 5). Third, a published code's meaning is immutable. If the trigger condition of PLAN_LIMIT_REACHED changes (say, from a monthly to a daily quota), add PLAN_DAILY_LIMIT_REACHED instead of quietly redefining the old code. Old clients and old agents depend on the old semantics; changing them plants a landmine in production.

Look at real-world benchmarks. Stripe's error body is widely considered the best-written in the industry:

{
  "error": {
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "type": "card_error",
    "message": "Your card has insufficient funds."
  }
}

Note its three layers: type is the broad category (card_error / api_error / idempotency_error), code is the stable machine code (snake_case), decline_code is the issuer's granular reason, and message is human copy. Each layer has its own job; none substitutes for another. Twilio uses numeric codes (e.g. 21211 for "invalid destination number") with one documentation page per code — numeric codes read poorly, but the "one code, one docs page" governance idea is worth borrowing (see section 9).

2. RFC 9457 Problem Details: The Standard "Shape" of an Error

Published in June 2023, RFC 9457, "Problem Details for HTTP APIs", officially replaced RFC 7807 as the standard shape for HTTP API error responses. It does one thing: it defines five core fields and the media type application/problem+json (plus application/problem+xml). Set Content-Type: application/problem+json on any error response, and consumers — including generic HTTP clients and agent frameworks — immediately know "this is a standard error body" and can parse fixed fields.

Diagram of RFC 9457's five fields: type/title stable per problem type (green), status mirroring the HTTP status (yellow), detail/instance per occurrence (blue), plus the allowed extension-members row

The five fields divide the labor precisely, each carrying one kind of information, never mixed:

HTTP/1.1 402 Payment Required
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/plan-limit-reached",
  "title": "Plan Limit Reached",
  "status": 402,
  "detail": "Monthly AI token quota of 1,000,000 exhausted; resets on 2026-11-01.",
  "instance": "/v1/projects/9f2a.../generate",
  "code": "PLAN_LIMIT_REACHED",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "balance": 0,
  "required": 1200,
  "retryAfter": 2592000
}
  • type: a URI reference identifying the problem type — "what kind of problem this is". It can be a real documentation page (as above) or a conventional URN. The RFC default is about:blank. Its job is to give humans and machines a "where to look it up" entry point — an agent receiving an unknown type can fetch that URI to learn its meaning.
  • title: a stable short summary of the problem type. The RFC requires the title to stay consistent across occurrences of the same problem, changing only for localization. So title is "the human-readable name of the problem type", not a description of this particular error. Never write per-occurrence prose like "user xxx ran out of quota" into title.
  • status: the HTTP status code mirrored inside the body. It exists for one reason: some proxies and log pipelines only capture bodies, and status makes the body self-contained. It must match the status line of the HTTP response — a mismatch creates dual-semantics chaos.
  • detail: a human-readable explanation specific to this occurrence; it may differ every time. But note: detail is for "the person reading logs", not the end user — end-user copy comes from the i18n dictionary (see section 4), while detail may carry technical specifics (quota numbers, reset times).
  • instance: a URI reference identifying this specific occurrence, usually the request path or an event ID. When a user reports "my instance is /v1/projects/…/generate", support can locate it instantly.

Three points that are often missed but matter. First, RFC 9457 explicitly permits extension members (like code, traceId, balance above) — a problem-type definition may declare its own extra fields, which is exactly what legitimizes the agent contract in section 7. Second, Content-Type must honestly say application/problem+json, not application/json, or consumers can't distinguish "a normal JSON response" from "a standard error body". Third, Spring Framework 6 ships a native org.springframework.http.ProblemDetail class for this spec; Express, NestJS and FastAPI need a small hand-rolled middleware/filter — three lines at heart: fix the shape, fix the Content-Type, one unified exit.

// Express: a unified problem+json exit (mount after all routes)
app.use((err, req, res, next) => {
  const status = err.status ?? 500;
  res.status(status).type('application/problem+json').json({
    type: err.problemType ?? 'about:blank',
    title: err.title ?? 'Internal Server Error',
    status,
    detail: status >= 500 ? undefined : err.detail, // never expose 5xx detail outward
    instance: req.originalUrl,
    code: err.code ?? 'INTERNAL_ERROR',
    traceId: req.traceId, // see section 5: request-scoped trace identifier
  });
});

3. HTTP Status Mapping: A Decision Table with Semantic Red Lines

Picking the wrong status code is the most common error-design accident. The table below covers 95% of API-project scenarios — follow it and you won't go wrong:

ScenarioStatusSemantic red line
Malformed JSON body, missing required fields400 Bad RequestPure "request shape" problems — the server never reached business logic
Missing/invalid credentials401 UnauthorizedMust include a WWW-Authenticate header telling the client how to authenticate; note 401's name is historical baggage — it really means "unauthenticated"
Authenticated but not authorized403 Forbidden401 is "who are you", 403 is "you're not allowed". Don't abuse 404-as-403 to "hide resource existence" — it drives debuggers mad
Resource doesn't exist404 Not FoundOnly for "can't locate the resource"; faking 404 for auth failures is an anti-pattern
Business-rule conflict (duplicate create, illegal state transition, idempotency-key conflict)409 Conflict409's essence: "this operation can't be done in the resource's current state" — the client may retry with different parameters or after the state changes; Idempotency-Key conflicts must return 409, never 400
Semantically valid but business validation fails422 Unprocessable ContentThe JSON is perfectly well-formed, but "email format invalid", "insufficient balance", "date in the past" — that's the 422/400 watershed. Note RFC 9110 renamed 422 from "Unprocessable Entity" to "Unprocessable Content"
Rate limiting / quota exceeded429 Too Many RequestsMust include a Retry-After response header (seconds or HTTP date, RFC 6585), or clients can only wait and retry blindly

A few high-frequency pitfalls, expanded. 422 vs 400: some frameworks (early Rails included) bucket all validation errors as 422, other teams use 400 for everything. Either is fine, but it must be consistent site-wide, and 422 bodies should carry structured validation details (see section 8). 403 vs 429 for quotas: is an exhausted plan quota a 403 or a 429? Follow the semantics — 429 means "it recovers automatically after a while" (paired with Retry-After), 403 means "your identity/plan says you can't". An exhausted monthly AI-token quota that resets next month is more honestly a 429 + Retry-After, letting clients queue until next month; "the free tier may not use this feature" is a pure 403. The king of anti-patterns: 200 wrapping {"success": false}. It breaks every infrastructure assumption: CDNs cache your "error", load-balancer health checks misjudge, fetch/axios error branches never run, Prometheus 5xx alerts never fire. One exception: some webhook consumers (e.g. WeChat Pay notifications) require a 200 or they retry frantically — that's a compromise inside that specific callback protocol, not a general API design.

4. i18n Copy Engineering: Title Is a Key, Not a Sentence

Internationalizing error copy isn't "find a translator to swap Chinese for English" — it's an engineering discipline: frontends and agents never consume natural-language copy for logic; copy only travels via dictionary keys; detail is content-negotiated per Accept-Language; English is the fallback.

The implementation separates three layers. First, title and error codes are stable dictionary keys — e.g. PLAN_LIMIT_REACHED maps to errors.PLAN_LIMIT_REACHED.title, with one dictionary per language, resolved by the frontend's current locale. Never hard-code a Chinese string for an includes check (the fragile pattern condemned in section 1). Second, detail is generated server-side via Accept-Language content negotiation — the backend owns i18n resources (errors.zh-CN.json / errors.en.json), and detail templates interpolate parameters (remaining quota, reset time) instead of concatenating strings. Third, the fallback: when a language lacks a translation, fall back to English, not to the raw key — users can still read English copy, but a naked PLAN_LIMIT_REACHED only confuses; meanwhile CI must guarantee the English dictionary covers 100% of error codes (see section 9).

// errors/en.json (English is the fallback language: must cover everything)
{
  "PLAN_LIMIT_REACHED": {
    "title": "Plan limit reached",
    "detail": "Monthly AI token quota of {quota} exhausted; resets on {resetDate}."
  }
}
// errors/zh-CN.json
{
  "PLAN_LIMIT_REACHED": {
    "title": "套餐额度已用尽",
    "detail": "本月 AI token 额度 {quota} 已用完,将于 {resetDate} 重置。"
  }
}

Two more details. First, detail templates use named parameters ({quota}), not positional ones, because word order differs across languages and positional parameters break in translation — the classic problem ICU MessageFormat solved; adopting the ICU format for dictionaries is recommended. Second, keep end-user copy separate from developer-facing detail: detail may read "quota=1000000, used=1000000, reset=2026-11-01", but the UI should show the dictionary's friendly copy plus an action ("Upgrade plan" button / "try again next month" hint). Mix them and you get either users who can't parse technical jargon, or developers who can't find key parameters in logs.

5. traceId Propagation: Layered Disclosure for 5xx

The most important field in an error response may not be the code but the traceId — the single bridge between "the error the user saw" and "the server logs". Without it, a user reporting "it errored just now" forces support to reconstruct steps from narration; with it, "error code PLAN_LIMIT_REACHED, trace ID 4bf92f35" lets support find the full call chain in the log system in ten seconds.

Generating and propagating traceId follows a standard recipe: a gateway/entry middleware mints a unique ID per request (UUID v4 or ULID both work) and does three things with it — puts it in the response body's traceId field, puts it in a response header (X-Request-Id is the de-facto standard from the Heroku/NGINX ecosystem), and injects it into the logging context (MDC/AsyncLocalStorage). If the system already runs OpenTelemetry, just use OTel's trace_id (32 hex chars, e.g. 4bf92f3577b34da6a3ce929d0e0e4736) and follow the W3C Trace Context spec to pass traceparent: 00-<trace-id>-<span-id>-01 between services — then the traceId doubles as the distributed-tracing ID, serving both user reports and trace analysis with one identifier.

5xx disclosure must be layered — this is a security red line: stack traces, SQL statements, file paths and environment variables go into server logs only, never into response bodies. A user-facing 5xx response should look like this:

HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json

{
  "type": "about:blank",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Something went wrong on our end. Please try again, or contact support with trace ID 4bf92f3577b34da6a3ce929d0e0e4736.",
  "instance": "/v1/projects/generate",
  "code": "INTERNAL_ERROR",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

Note the detail says only "please retry / contact support + trace ID" — zero technical specifics. In server logs, the same traceId carries the full stack, request parameters (redacted), and SQL. Implementation is simple: a global exception filter catches unhandled errors, logs them with the traceId and stack, then returns the fixed shape above. NestJS uses a global @Catch() ExceptionFilter, Express uses error-handling middleware, FastAPI uses @app.exception_handler(Exception) — the pattern is identical everywhere: verbose in logs, restrained in responses.

6. Frontend TypeScript: Discriminated Unions to Kill String Matching

With stable error codes, the frontend can finally drop message.includes('quota'). The right modeling is a TypeScript discriminated union keyed on code:

// errors.ts — each error code is a type; a new code is a new union member
type ApiError =
  | { code: 'PLAN_LIMIT_REACHED'; status: 402; balance: number; required: number; retryAfter: number; traceId: string }
  | { code: 'AUTH_TOKEN_EXPIRED'; status: 401; traceId: string }
  | { code: 'QUOTA_RATE_LIMITED'; status: 429; retryAfter: number; traceId: string }
  | { code: 'VALIDATION_FAILED'; status: 422; errors: FieldError[]; traceId: string }
  | { code: 'INTERNAL_ERROR'; status: 500; traceId: string };

// UI layer: exhaustive switch — miss a branch and the compiler complains
function handleApiError(err: ApiError) {
  switch (err.code) {
    case 'PLAN_LIMIT_REACHED':
      return showUpgradeDialog({ balance: err.balance, required: err.required });
    case 'AUTH_TOKEN_EXPIRED':
      return redirectToLogin();
    case 'QUOTA_RATE_LIMITED':
      return showRetryCountdown(err.retryAfter);
    case 'VALIDATION_FAILED':
      return highlightFields(err.errors);
    case 'INTERNAL_ERROR':
      return showGenericError(err.traceId); // generic fallback + trace ID
  }
}

Three wins. First, switch exhaustiveness is compiler-checked — when the backend adds a code and the frontend's ApiError type isn't synced, TypeScript errors out instead of silently falling into a default branch in production. Second, retryability becomes a pure function instead of magic numbers scattered around:

// Retryable = explicitly transient errors; unknown codes are never blindly retried
const RETRYABLE = new Set(['QUOTA_RATE_LIMITED', 'INTERNAL_ERROR', 'SERVICE_UNAVAILABLE']);
function isRetryable(code: string): boolean {
  return RETRYABLE.has(code);
}
// Caller: exponential backoff that honors Retry-After
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    try { return await fn(); }
    catch (e) {
      const code = (e as ApiError).code;
      if (attempt >= maxAttempts || !isRetryable(code)) throw e;
      const waitMs = (e as ApiError & { retryAfter?: number }).retryAfter
        ? retryAfter * 1000 : 2 ** attempt * 1000;
      await sleep(waitMs);
    }
  }
}

Third, a fallback for unknown codes is mandatory: any unrecognized code goes down the "generic copy + show traceId + log it" path — never a blank screen, never a swallowed error. This belongs on the launch checklist (see section 10), because backends shipping new codes while frontends lag behind is the norm, and the fallback is the last line of defense decoupling frontend/backend release cadences.

7. The Agent Error Contract: Making Errors "Executable"

When the consumer changes from human to AI agent, the design goal upgrades: an agent should never need a second request just to "understand" an error. A human seeing "insufficient balance" clicks "Top up"; an agent needs machine-consumable decision parameters. That's where RFC 9457's extension members earn their keep — alongside the five standard fields, carry agent-actionable extension fields:

{
  "type": "https://api.example.com/problems/plan-limit-reached",
  "title": "Plan Limit Reached",
  "status": 402,
  "code": "PLAN_LIMIT_REACHED",
  "detail": "Monthly AI token quota exhausted.",
  "instance": "/v1/projects/generate",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "balance": 0,          // remaining quota: machine-readable
  "required": 1200,      // quota this operation needs: machine-readable
  "retryAfter": 2592000, // seconds until next reset: machine-readable
  "action": "upgrade_or_wait"  // suggested action enum: upgrade | wait | contact_support
}

With this body, the agent's decision chain is deterministic, no LLM guessing required: balance(0) < required(1200) → this operation can never succeed → action=upgrade_or_wait → check retryAfter to decide between "wait 30 days" (unrealistic, escalate) and "suggest the user upgrades". Without extension fields, the agent can only feed detail's prose to an LLM, guess "probably out of quota", then maybe call a "check quota" endpoint to confirm — one more request, one more hallucination opportunity.

Three principles for agent-facing error contracts. First, extension fields use machine units, not human units. retryAfter is seconds (2592000), not "in 30 days"; money is in the smallest currency unit, not a "¥12.00" string — timezones and conversions belong to the consumer. Second, suggested actions are closed enums, not prose. action is one of retry | upgrade | wait | contact_support | fix_input, matched with a switch; a natural-language "suggestedFix" can only assist an LLM, never drive a decision. Third, the error body must be describable in OpenAPI. Declare extension fields in the OpenAPI schema (or an x-error-codes extension, see section 9) so an agent's tool description directly shows "which error codes this tool may return and which fields each carries" — the key to agents preparing for errors before they even call.

8. 422 Validation Details: Structured Error Lists

Form/parameter validation errors deserve their own section — they're the easiest part of a 422 response to get wrong. The anti-pattern concatenates every failure into one string: "detail": "invalid email; password too short; age must be a number" — and the frontend, wanting to pin errors under the right inputs, has to write regexes to "parse" that prose. The right approach is an errors array with JSON Pointer field addressing:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "code": "VALIDATION_FAILED",
  "instance": "/v1/signup",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "errors": [
    { "field": "email", "pointer": "#/data/email", "code": "FORMAT_INVALID", "message": "Invalid email format" },
    { "field": "password", "pointer": "#/data/password", "code": "TOO_SHORT", "message": "Password must be at least 8 characters" },
    { "field": "age", "pointer": "#/data/age", "code": "TYPE_MISMATCH", "message": "Age must be a number" }
  ]
}

Each field has its job: field is the flat name the frontend form binds to; pointer is an RFC 6901 JSON Pointer (e.g. #/data/email) pinpointing the nested path inside the request body — nested objects and array-element errors (like #/data/items/2/price) can only be expressed via pointer; code is the validation-rule code (FORMAT_INVALID / TOO_SHORT / REQUIRED / TYPE_MISMATCH, following the same naming convention); message is the per-item copy already localized per Accept-Language, rendered straight under the field with no re-translation needed.

One production safeguard is mandatory: batch-validation truncation. A 10,000-row CSV import with 5 errors per row naively returns 50,000 error entries — the body sails past 1MB, the frontend chokes parsing it, the log pipeline explodes. Cap it server-side (e.g. MAX_VALIDATION_ERRORS = 20); on overflow, truncate and append a meta entry: {"code": "TOO_MANY_ERRORS", "message": "showing first 20 of 532 errors"}. Validation is sampling feedback, not a full audit — users fix the first 20 and resubmit.

9. Error-Code Governance: Registry + CI + Docs Site

Past ~30 error codes, verbal agreements and string constants scattered through code will rot. You need three things: a registry, CI checks, and a docs site.

Registry has two pragmatic forms. The lightweight form: an errors/ directory in the repo, one YAML per code, declaring the code value, HTTP status, bilingual titles, detail templates, extension fields, and the version it was introduced in:

# errors/PLAN_LIMIT_REACHED.yaml
code: PLAN_LIMIT_REACHED
httpStatus: 402
title: { zh: "套餐额度已用尽", en: "Plan limit reached" }
detailTemplate:
  zh: "本月 AI token 额度 {quota} 已用完,将于 {resetDate} 重置。"
  en: "Monthly AI token quota of {quota} exhausted; resets on {resetDate}."
extensions: [balance, required, retryAfter, action]
since: "v2.4.0"
deprecated: false

The heavier form declares error codes in the OpenAPI document via an x-error-codes extension under each operation, listing the codes that operation may return with each code's extension-field schema — so agent tool descriptions can be generated straight from OpenAPI (echoing section 7). The two aren't exclusive: the YAML registry is the "source of truth", the OpenAPI extension is the "public contract", and CI keeps them consistent.

CI checks are the real anti-rot defense. Add a check script to the PR pipeline with blunt rules: a new error code must ship with a YAML registry entry, zh+en dictionary entries, and a docs-page stub; httpStatus must come from the section-3 decision table's allowed set; deprecated: true codes may not appear at new throw sites; the YAML code must match the string constant actually thrown in code, character for character (verified both ways with grep/AST scans). The first run will surface a pile of historical debt — that's normal; paying it down is what governance means.

The docs site can be generated from the registry: one page per error code with its value, meaning, trigger scenarios, example response, and three perspectives — "what the user sees / what the developer sees / what the agent should do". Twilio's error-code docs are the benchmark here — its numeric codes are unreadable by themselves, but "one page per code + search" makes unreadable usable. Once the URL pattern is fixed, it can double as the RFC 9457 type field value (e.g. https://docs.example.com/errors/PLAN_LIMIT_REACHED), closing the "error body → documentation" loop.

10. Launch Checklist: Tick Every Box Before Shipping

Converge everything to verify before launch into one checklist, pasted into the PR template:

  • Unknown-code fallback: the frontend routes every unlisted code to a generic error page (friendly copy + traceId + "contact support" entry) and emits an unknown_error_code log for later registration. Test: mock a code the backend hasn't shipped and check the UI doesn't go blank.
  • Log redaction: detail, logged request params and error contexts must never contain emails, passwords, tokens or ID numbers. Ship a redact() utility applied before logging and before building detail; add a regex scan in CI that fails on patterns like plaintext password.
  • Zero 5xx leakage: fuzz every endpoint and assert 500 bodies never contain stack, sql or absolute filesystem paths. This is a security red line, not a UX nicety.
  • Canary compatibility: when error-code changes (new codes, detail-template revisions) roll out via canary, old frontends/agents must still handle new codes — that's what the section-6 unknown-code fallback is for. Watch the volume of unknown_error_code logs during canary; a spike means some client didn't keep up.
  • Retry-After actually works: 429 responses must carry the Retry-After header, equal to the body's retryAfter extension field; write an integration test asserting the two match.
  • Correct Content-Type: every error response declares application/problem+json, not application/json. Gateway-thrown errors (NGINX, Cloudflare Workers) must share the same shape — no double standard where "gateway 502 is HTML but app 500 is JSON".
  • Error docs reachable: curl every documentation URI in type fields and assert 200 — a dead link blocks the "where to look it up" entry point.

Back to the opening line: errors are the most frequently invoked interface asset. A good error system has three litmus tests — dare the frontend to delete every message.includes (daring means the codes are stable enough); can support locate an issue from a single pasted traceId (yes means propagation is right); can an agent decide correctly without guessing or a second request (yes means the extension fields suffice). Pass all three and your error system won't rot in five years. What remains is section 9's governance work — registry, CI, docs site — turning time into an ally instead of an enemy.

Views 0Comments 0

Comments (0)

ME
0/1000
Loading comments...
Browse projectsPublish your project

Related articles