// Robust Anthropic error handling (TypeScript SDK): typed errors, retry matrix, request ids, streaming errors. // STATUS: LIVE_VERIFIED 2026-09-18 (deliberate 404 + 400 on claude-haiku-4-5-20251001; nothing billed). // Run: node --env-file=.env --experimental-strip-types examples/shared/errors/anthropic_error_handling.ts import Anthropic from "@anthropic-ai/sdk"; import { appendFileSync } from "node:fs"; const client = new Anthropic({ maxRetries: 0, timeout: 60_000 }); // production: keep default maxRetries 2 (408/409/429/5xx + connection errors, honors retry-after) const MODEL = "claude-haiku-4-5-20251001"; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const log = (status: number, note: string) => appendFileSync("reports/live-requests.jsonl", JSON.stringify({ ts: new Date().toISOString().replace(/\.\d+Z$/, "Z"), provider: "anthropic", method: "POST", path: "/v1/messages", status, est_cost_usd: 0, note }) + "\n"); function isFatal(e: Anthropic.APIError): boolean { return e instanceof Anthropic.BadRequestError || e instanceof Anthropic.AuthenticationError || e instanceof Anthropic.PermissionDeniedError || e instanceof Anthropic.NotFoundError || e instanceof Anthropic.UnprocessableEntityError; } async function withBackoff(fn: () => Promise, attempts = 4, base = 500, cap = 8000): Promise { for (let i = 0; ; i++) { try { return await fn(); } catch (e) { if (!(e instanceof Anthropic.APIError)) throw e; const shouldRetry = e.headers?.get?.("x-should-retry"); // live: "false" on every 4xx const retryAfter = e.headers?.get?.("retry-after"); if (isFatal(e) || shouldRetry === "false" || i === attempts - 1) throw e; const delay = retryAfter ? Number(retryAfter) * 1000 : Math.min(cap, base * 2 ** i) * (1 + Math.random() * 0.25); console.log(` retryable ${e.status} ${e.name} (request-id ${e.requestID}); sleeping ${Math.round(delay)}ms`); await sleep(delay); } } } async function demo(label: string, fn: () => Promise) { try { console.log(`${label}: ok ->`, await fn()); } catch (e) { if (e instanceof Anthropic.APIError) { const err = (e.error as any)?.error ?? {}; console.log(`${label}: HTTP ${e.status} ${e.constructor.name} type=${err.type} msg=${JSON.stringify(err.message)} request_id=${e.requestID}`); log(e.status ?? 0, `error-handling demo ${label}`); } else if (e instanceof Anthropic.APIConnectionError) { console.log(`${label}: connection problem`, e.name, e.message); } else throw e; } } await demo("404 unknown model", () => client.messages.create({ model: "claude-does-not-exist", max_tokens: 8, messages: [{ role: "user", content: "Reply with OK." }] })); await demo("400 bad temperature", () => client.messages.create({ model: MODEL, max_tokens: 8, temperature: 5, messages: [{ role: "user", content: "Reply with OK." }] })); // Streaming: `error` events after HTTP 200 surface as thrown APIError / 'error' event on MessageStream. try { const stream = client.messages.stream({ model: MODEL, max_tokens: 8, messages: [{ role: "user", content: "Reply with OK." }] }); stream.on("error", (err) => console.log("stream error event:", err.name)); const final = await stream.finalMessage(); console.log("stream ok:", final.content[0]?.type === "text" ? final.content[0].text.trim() : "", "| stop_reason:", final.stop_reason); log(200, "error-handling demo stream"); } catch (e) { console.log("stream failed:", (e as Error).name, (e as Error).message); }