{"post":{"slug":"how-to-handle-api-errors-2026","title":"How to handle API errors in 2026","excerpt":"Unhandled API errors are the leading cause of silent data loss and broken UX. This 2026 guide covers every layer from network faults to typed error objects.","category":"code","date":"March 31, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">E</span>very API call can fail, and the question is not if but when. In production, networks partition, rate limits hit, services restart, and auth tokens expire. How your code handles those moments determines whether users see a graceful message or a blank screen — and whether on-call engineers have enough context to fix the problem in minutes or hours.\n\n## What changed in 2026\n\n- **Typed fetch clients are default.** `openapi-fetch`, `zodios`, and Hono's RPC layer generate typed error shapes from your OpenAPI spec, catching mismatches at compile time.\n- **AI services add new failure modes.** LLM APIs time out on long generations, return streaming chunks that error mid-stream, and hit concurrency limits. Classic HTTP error handling needs extension.\n- **Edge runtimes are everywhere.** Cloudflare Workers and Vercel Edge run on limited runtimes — no `node:net`, limited retry windows, 50 ms CPU budget. Error handling must be lightweight.\n- **OpenTelemetry is the standard.** Errors land in distributed traces automatically if you propagate the context; correlation IDs are no longer optional.\n\n## HTTP status classes\n\n| Class | Range | Meaning | Retry? |\n| --- | --- | --- | --- |\n| 2xx | 200–299 | Success | N/A |\n| 4xx | 400–499 | Client error | No (fix the request) |\n| 429 | Too Many Requests | Rate limited | Yes, after `Retry-After` header |\n| 5xx | 500–599 | Server error | Yes, with back-off |\n| Network | `fetch` throws | DNS/TLS/timeout | Yes, limited attempts |\n\nNever retry a 400 or 404 — the server is telling you that *your request* is wrong.\n\n## Exponential back-off with jitter\n\n```typescript\nasync function fetchWithRetry<T>(\n  url: string,\n  options: RequestInit,\n  maxAttempts = 4,\n): Promise<T> {\n  for (let attempt = 0; attempt < maxAttempts; attempt++) {\n    const res = await fetch(url, options);\n\n    if (res.ok) return res.json() as Promise<T>;\n\n    // Don't retry client errors (except 429)\n    if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n      throw new ApiError(res.status, await res.text());\n    }\n\n    if (attempt === maxAttempts - 1) throw new ApiError(res.status, \"max retries\");\n\n    // Exponential back-off with full jitter\n    const base = Math.min(1000 * 2 ** attempt, 30_000);\n    const delay = Math.random() * base;\n    await new Promise((r) => setTimeout(r, delay));\n  }\n  throw new Error(\"unreachable\");\n}\n```\n\nJitter prevents the \"thundering herd\" problem where all clients retry simultaneously after an outage.\n\n## Typed errors in TypeScript\n\n```typescript\ntype ApiSuccess<T> = { ok: true; data: T };\ntype ApiFailure = { ok: false; status: number; message: string };\ntype ApiResult<T> = ApiSuccess<T> | ApiFailure;\n\nasync function getUser(id: string): Promise<ApiResult<User>> {\n  try {\n    const res = await fetch(`/api/users/${id}`);\n    if (!res.ok) {\n      return { ok: false, status: res.status, message: await res.text() };\n    }\n    return { ok: true, data: await res.json() };\n  } catch (err) {\n    return { ok: false, status: 0, message: \"network error\" };\n  }\n}\n\n// Caller is forced to handle both cases\nconst result = await getUser(\"123\");\nif (!result.ok) {\n  console.error(\"Failed:\", result.message);\n  return;\n}\nconsole.log(result.data.name);\n```\n\nThis discriminated union makes the error path impossible to accidentally skip.\n\n## How to pick an error strategy\n\n| Scenario | Strategy |\n| --- | --- |\n| User-facing UI action | Show specific message, offer retry button |\n| Background job | Retry with back-off, dead-letter after N failures |\n| Webhook receiver | Return 200 immediately, process async, retry internally |\n| Streaming (SSE/LLM) | Detect mid-stream error, close gracefully, show partial result |\n| Critical write (payment) | Idempotency key + exactly-once check before retry |\n\n## Logging and observability\n\n```typescript\nimport { trace, context } from \"@opentelemetry/api\";\n\nasync function callExternalApi(payload: unknown) {\n  const span = trace.getActiveSpan();\n  try {\n    const res = await fetch(\"https://api.example.com/process\", {\n      method: \"POST\",\n      body: JSON.stringify(payload),\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n    if (!res.ok) {\n      const body = await res.text();\n      span?.setStatus({ code: 2, message: body }); // ERROR\n      span?.setAttribute(\"http.status_code\", res.status);\n      throw new ApiError(res.status, body);\n    }\n    return res.json();\n  } catch (err) {\n    span?.recordException(err as Error);\n    throw err;\n  }\n}\n```\n\nAlways attach the HTTP status code, the request path, and any correlation ID the server returns. Redact sensitive fields before logging.\n\n## Common mistakes\n\n**Catching and swallowing.** `catch (e) {}` means errors disappear silently. At minimum, log or re-throw.\n\n**Retrying non-idempotent requests.** Retrying a `POST /orders` creates duplicate orders. Attach an idempotency key header (`Idempotency-Key: <uuid>`) for all mutations.\n\n**Not handling `fetch` throws.** `fetch` throws on network failure, not on 4xx/5xx. Always wrap in try/catch *and* check `res.ok`.\n\n**No timeout.** A hung request blocks UI or a worker thread forever. Use `AbortController` with a deadline:\n\n```typescript\nconst controller = new AbortController();\nconst timeout = setTimeout(() => controller.abort(), 10_000);\nconst res = await fetch(url, { signal: controller.signal });\nclearTimeout(timeout);\n```\n\n## What to skip\n\n- **Generic error boundaries that hide all errors** — use them for truly unexpected crashes, not expected API failures with recoverable state.\n- **Polling status endpoints without back-off** — you will hammer the server during outages; use exponential back-off or webhooks.\n- **Rolling your own HTTP client from scratch** — `ky`, `ofetch`, or `axios` (with interceptors) already handle retries, timeout, and JSON parsing.\n\n## FAQ\n\n**Should I throw errors or return result types?**\nBoth have valid uses. Throw for truly unexpected errors; return result types (discriminated unions) for expected failures you want callers to handle explicitly.\n\n**How long should I wait between retries?**\nStart at ~500 ms, double each attempt, cap at ~30 s. Add jitter of ±50 % to spread load.\n\n**What is an idempotency key?**\nA unique ID (usually a UUID) you attach to a request. The server deduplicates: if it sees the same key twice, it returns the first response instead of processing again.\n\n**How do I test error handling?**\nUse `msw` (Mock Service Worker) to simulate 429s, 500s, and network timeouts in unit and integration tests — no real server needed.\n\n## Where to go next\n\n- [How to mock an API in 2026](/blog/how-to-mock-an-api-2026)\n- [How to build a REST API in 2026](/blog/how-to-build-a-rest-api-2026)\n- [How to write clean functions in 2026](/blog/how-to-write-clean-functions-2026)","hue":319,"metaTitle":"How to Handle API Errors in 2026 (Complete Guide)","metaDescription":"2026 guide to API error handling: retry strategies, typed errors, exponential back-off, logging, and patterns that keep production apps reliable.","keywords":["how to handle api errors 2026","api error handling 2026","exponential backoff javascript 2026","typescript api error types 2026","http error handling best practices 2026","retry logic api 2026","rest api error handling 2026","fetch error handling 2026","api error logging 2026","robust api client 2026"],"tldr":["**Distinguish error classes** — network faults, 4xx client errors, and 5xx server errors all need different handling strategies.","**Retry with exponential back-off** for transient failures; never retry 4xx errors without changing the request.","**Type your errors** — a discriminated union in TypeScript forces you to handle every case at compile time.","**Surface meaningful context** — log correlation IDs, request bodies (redacted), and stack traces; never swallow errors silently.","**Skip:** global `try/catch` that converts all errors to a generic \"Something went wrong\" — you lose debuggability forever."],"readTime":"5 min read","wordCount":1005},"related":[{"slug":"how-to-handle-errors-gracefully-2026","title":"How to handle errors gracefully in 2026: patterns for resilient services","excerpt":"Error handling is the difference between a system that fails safely and one that fails silently or catastrophically. This 2026 guide covers error types, propagation patterns, and the practices that make services observable and resilient.","category":"code","date":"March 31, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":320},{"slug":"error-handling-explained-2026","title":"Error handling explained in 2026: stop letting exceptions crash your users","excerpt":"Poor error handling is the leading cause of poor user experience and silent data corruption. This 2026 guide covers strategies, typed errors, and production patterns.","category":"code","date":"March 27, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309},{"slug":"how-to-build-a-rest-api-2026","title":"How to build a REST API in 2026: the complete guide","excerpt":"REST APIs are still the backbone of most web backends in 2026. This guide covers design, implementation, validation, auth, and deployment correctly.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":322}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-handle-a-difficult-coworker-2026","title":"How to handle a difficult coworker in 2026: scripts and real steps","excerpt":"Difficult coworkers drain energy and derail teams. Here are concrete steps, word-for-word scripts, and the escalation ladder for 2026 workplaces.","category":"guides","date":"March 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":130},"older":{"slug":"how-to-handle-errors-gracefully-2026","title":"How to handle errors gracefully in 2026: patterns for resilient services","excerpt":"Error handling is the difference between a system that fails safely and one that fails silently or catastrophically. This 2026 guide covers error types, propagation patterns, and the practices that make services observable and resilient.","category":"code","date":"March 31, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":320}}