What You Need to Know
A Claude application can fail in two places. The integration layer is everything between your code and a valid response. It covers authentication, request construction, size and schema validity, rate limiting, transport, timeouts, stream assembly and your own parsing of the reply. Model output is what goes wrong once a well-formed request has been answered with HTTP 200: content that is wrong, incomplete, off-format or fabricated. An error type is the exact string a failed response names, such as rate_limit_error. A stop reason is the stop_reason field on a successful response, which says why generation ended. Debugging means finding the layer, then the class, then the recovery for that class.
Take a roofing firm whose Claude service turns surveyors' roof inspection notes into customer quotes. It loads the current materials price list into the context and calls a price-lookup tool for each item. In one week, some quote requests fail with an error, some quotes stop mid-sentence, and one quote shows a slate price from no price list.
Isolate the Layer Before You Theorise
The fixes for the two layers do not overlap. A prompt rewrite cannot repair a bad credential, and a new credential cannot repair a fabricated figure. Treating one layer as the other is the most common wasted debugging day there is.
Pick the layer from evidence. A 4xx or 5xx status with an error body naming an error.type comes from the integration layer. A 200 whose content you dislike comes from the prompt or the model. Anthropic's stop-reason page draws the same line. Stop reasons sit inside a successful response body, and the content is valid. Errors are 4xx or 5xx request failures. The roofing firm's failed requests carry a status and an error.type, so they belong to the integration layer.
Systems that retrieve documents into the context face one more question before either verdict. Was the material assembled into the context window correct? A better prompt over the wrong documents only buys a more confident wrong answer, such as a quote at last year's prices.
Log the discriminators, then theorise
Request id, HTTP status, error.type, stop_reason, usage, raw content. Capture all six fields on successes as well as failures.
Error Types, and the Split That Picks the Recovery
The type strings are exact. As of August 2026, these are the documented status codes and their types.
- 400
invalid_request_error, which the docs note "may also be used for other 4XX status codes not listed in this section" - 401
authentication_error - 402
billing_error - 403
permission_error - 404
not_found_error - 409
conflict_error - 413
request_too_large - 429
rate_limit_error - 500
api_error - 504
timeout_error - 529
overloaded_error
Every error body has the same shape: a top-level type of error, a nested error object that always includes a type and a message, and a request_id.
{
"type": "error",
"error": {
"type": "not_found_error",
"message": "The requested resource could not be found."
},
"request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}Deterministic faults are properties of the request you sent: 400, 401, 402, 403, 404 and 413. The identical request fails identically however many times you send it. Transient faults are properties of the moment, and they are the retryable class: 429, 500, 504, 529 and raw connection failures. At the roofing firm, one surveyor's quote fails with a 413 on every attempt, at any hour. Other requests fail with a 429 only on Monday mornings, when the weekend's notes all go at once.
409 conflict_error sits in the SDKs' retried set but is not transient. The docs prescribe "resolve the conflict, then retry the request", so it is a fix-then-retry fault that backoff alone does not clear.
The SDKs retry connection errors, 408, 409, 429 and 500 and above "with exponential backoff, twice by default, honoring the retry-after header when present". You tune this through max_retries or maxRetries. The SDKs also retry the two further exceptions covered below: a 504 on the longest requests, and the 429 a spend cap returns.
Branch on typed classes rather than error strings, most specific first. RateLimitError is a subclass of APIStatusError, so catching the parent first swallows the 429 silently. The handler below shows the order.
import anthropic
client = anthropic.Anthropic()
def summarise(instrument_log: str):
try:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": instrument_log}],
)
except anthropic.RateLimitError as exc: # 429, most specific first
headers = exc.response.headers
print("429", headers.get("retry-after"),
headers.get("anthropic-ratelimit-input-tokens-remaining"),
headers.get("request-id"))
raise
except anthropic.APIStatusError as exc: # every other non-2xx, 4xx and 5xx alike
print(exc.status_code, exc.body, exc.response.headers.get("request-id"))
raise
except anthropic.APIConnectionError as exc: # no status: it never reached the API
print("transport failure", exc.__cause__)
raise
print(message._request_id, message.stop_reason, message.usage)
return messageThe documented Python and TypeScript classes all descend from APIError.
- 400
BadRequestError - 401
AuthenticationError - 403
PermissionDeniedError - 404
NotFoundError - 409
ConflictError - 422
UnprocessableEntityError - 429
RateLimitError - 500 and above
InternalServerError APIConnectionErrorfor transport failures
The table names no class for 402 or 413, so read those off status_code on the generic APIStatusError. Class names and namespaces differ by language, so the table is not universal. Java raises com.anthropic.errors.NotFoundException. Go returns a single *anthropic.Error you branch on by StatusCode.
A rate-limit 429 carries its own evidence. retry-after gives "the number of seconds to wait until you can retry the request. Earlier retries will fail". The anthropic-ratelimit-*-remaining headers show the headroom. Limits use a token bucket, so a short burst can trip a ceiling the per-minute average never approaches. That is the roofing firm's Monday morning. An organisation-wide jump in usage produces 429s from acceleration limits, and the fix is ramping traffic gradually.
Spend limits break both halves of the split. As of September 2026, an organisation that reaches its usage tier's monthly spend cap gets a 429 rate_limit_error with no retry-after header. Its error.details.error_code reads enforced_spend_limit_reached, the field the docs say to use "to tell this response apart from a rate limit". Retrying, the SDKs' automatic retries included, "fails until access resumes": 00:00 UTC on the first of the next month, or sooner on a higher tier. The status code says transient, but no retry clears it.
A spend limit you set yourself, for the organisation or a workspace, returns a 400 invalid_request_error. Its message says the limit was reached and when access resumes. Limits on the Claude Code workspace can return a 429 instead. It looks like a request fault, but the request is fine. Raise or remove the limit.
A 200 Is Not Proof of Success
A 200 says the request was valid. stop_reason then says whether you have a complete answer, a truncated one, a paused one, or a refusal. Seven values are documented.
end_turn is a natural finish. stop_sequence means one of yours fired. tool_use means run the tools and send tool_result blocks back. max_tokens means truncated at your cap, so raise the cap or narrow the task. If it cut a tool_use block in half, retry with a higher cap.
pause_turn means the server-side sampling loop hit its iteration limit while executing server tools such as web search, ten per request by default. A response leaving a client tool_use block waiting on you never carries it. To continue, send the response back as-is with the same tools, because a paused turn can end on a server tool that has not run yet. Cap the continuations, since a continued turn can pause again.
refusal means safety classifiers declined. It arrives "as a normal HTTP 200 response, not an error", with stop_details naming the category. Its recovery is a different model, covered below. model_context_window_exceeded means generation stopped at the model's context-window limit rather than at your cap. That lets you request the maximum possible tokens without knowing the exact input size. Treat the response as truncated. Raising max_tokens provably does not help.
The field turns a vague symptom into a class. The roofing firm's short quotes all returned 200, with stop_reason reading max_tokens. So "the quotes stop mid-sentence" is a configuration fault with a configuration fix. No prompt rewrite or model swap touches it.
Streaming hides that field and adds a failure mode. stop_reason is null in message_start, supplied in message_delta, and provided in no other event. A client that never reads message_delta is blind to truncation by construction. Over SSE, "an error can occur after the API returns a 200 response. In that case, error handling doesn't follow these standard mechanisms". The frame is event: error, carrying something like overloaded_error. The function below handles both.
async function summarise(client: Anthropic, instrumentLog: string) {
const stream = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: instrumentLog }],
stream: true,
});
let text = "";
let stopReason: string | null = null;
try {
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
text += event.delta.text;
} else if (event.type === "message_delta") {
stopReason = event.delta.stop_reason; // the only event that carries it
}
// Unknown event types, including ones added later, are ignored on purpose.
}
} catch (err) {
// An error frame arriving long after the 200: log the partial as a failure
// rather than handing it back as an answer.
console.error("mid-stream failure", { chars: text.length, err });
return { text, stopReason, complete: false };
}
return { text, stopReason, complete: stopReason === "end_turn" };
}Recovering from a mid-stream break depends on the version. On Claude 4.5 and earlier the captured partial goes into an assistant message. On Claude 4.6 and later it goes into a user message with an instruction to continue. Either way, "tool use and extended thinking blocks cannot be partially recovered. You can resume streaming from the most recent text block."
Wrapping the call is not wrapping the response
A try/except around a streaming call catches connection and 4xx failures and nothing else. An overload arriving as an error frame mid-stream, or a truncation arriving as stop_reason max_tokens, walks straight past a call-level handler. Both get reported upstream as a short success.
Tool Failures Are Their Own Class
A tool that raises, returns nothing, or returns an unstructured blob leaves the model to improvise. An agent that then loops or fabricates has a tool-contract defect, not a quality problem. At the roofing firm, the price-lookup tool returned an empty string for slate, and the quote carried a price Claude made up.
The recovery is a tool_result carrying tool_use_id, content, and is_error set to true "if the tool execution resulted in an error". The content has to be worth reading: "Instead of generic errors like failed, include what went wrong and what Claude should try next (for example, Rate limit exceeded. Retry after 60 seconds.)". Given the specific message, Claude recovers. Given failed, it guesses. The firm's tool should say no slate price was found, and what to try next. Server-tool errors are the exception. The API handles them transparently, so you never set is_error for those.
A tool-result turn can also come back empty. As of September 2026 the stop-reasons page documents replies of two or three tokens with no content and stop_reason end_turn, mostly straight after tool results. A common cause is your own message construction. Text blocks added after the tool_result blocks are valid. But they teach Claude to expect the user to add text after every tool result, so it ends its turn. The fix is to send the tool results on their own. Re-sending the empty response unchanged does nothing, and a continuation prompt in a new user message is the last resort.
Match the Recovery to the Class
Transient faults get capped exponential backoff with jitter, honouring retry-after. Jitter spreads the waits at random. Without it, synchronised retries from a flushed backlog re-create the burst that caused the 429, as the roofing firm's Monday queue would.
import random
import time
import anthropic
client = anthropic.Anthropic(max_retries=0) # own the schedule instead of the SDK default of 2
def spend_capped(exc: anthropic.APIStatusError) -> bool:
body = exc.body if isinstance(exc.body, dict) else {}
details = (body.get("error") or {}).get("details") or {}
return details.get("error_code") == "enforced_spend_limit_reached"
def is_transient(exc: anthropic.APIError) -> bool:
if isinstance(exc, anthropic.APIConnectionError):
return True # timeouts included
if not isinstance(exc, anthropic.APIStatusError):
return False
if exc.status_code == 429:
return not spend_capped(exc) # the spend-cap 429 fails until access resumes
return exc.status_code >= 500
def with_backoff(call, attempts: int = 5, base: float = 1.0, cap: float = 30.0):
for attempt in range(attempts):
try:
return call()
except anthropic.APIError as exc:
if not is_transient(exc) or attempt == attempts - 1:
raise # deterministic or exhausted: fail loudly, never a silent stale answer
response = getattr(exc, "response", None)
after = response.headers.get("retry-after") if response is not None else None
delay = float(after) if after else min(cap, base * 2 ** attempt)
time.sleep(delay + random.uniform(0, delay * 0.25))A 504 on the longest requests is an exception to backoff. As of September 2026 the errors page answers timeout_error with "Consider using the streaming Messages API for long-running requests". Its long-requests guidance warns against a large max_tokens without streaming or the Message Batches API. If the timeouts cluster on the longest generations rather than under load, the cause is request length. Every retry restarts the same long generation from the beginning. Stream the request when someone is waiting for it. Batch it when nobody is.
Deterministic client faults get fixed and re-issued. A 401 needs a credential. A 413 needs a smaller payload, or the Files or Batch endpoint with its larger maximum. A 400 needs the request corrected. Retrying any of them unchanged spends money and latency to reproduce the same failure.
Schema-invalid output gets re-asked with the specific validation errors attached, which tell Claude what to change. It is never re-asked identically. The structural fix is constrained decoding through output_config.format with a json_schema, or strict: true on a tool definition. An unsupported schema feature returns "a 400 error with details". That 400 is itself the discriminator between a schema defect and a model defect.
A refusal gets a different model. Re-sending the request to the same model "usually earns another refusal". As of September 2026 the docs give three routes.
- Server-side fallback through the
fallbacksparameter (in beta on the Claude API) - The refusal-fallback middleware every Anthropic SDK ships
- A retry you write yourself
A refusal can arrive before any output or mid-stream. Either way the partial output is incomplete, so throw it away. A refusal is an HTTP 200, so monitoring built on error rates never sees it. Count refusals as their own signal.
Persistent quality failures get a design change rather than a retry. Truncation is a budget problem, format drift a schema-enforcement problem, and fabrication a grounding problem. Fix fabrication by letting Claude say it does not know and making every claim carry a supporting quote.
Two questions apply to every recovery. Idempotency: a retry that re-executes a side-effecting tool call is a bug, such as emailing a customer the same quote twice. Exhaustion: degrade explicitly or fail loudly, because serving the last run's output as though it were fresh turns a visible outage into an invisible one.
Traces That Reproduce, and the Eval That Proves the Fix
A usable trace lets someone reproduce the failure without the original operator. It holds the request id, the exact request sent, the raw response, stop_reason, usage, every tool call with its inputs and results, and the retry history. The request must include the assembled context rather than your template, because the template does not show what Claude received. If a failure cannot be reproduced from the logs, the logging is the first defect to fix. Never drop the request id. It is the only identifier shared between your logs and Anthropic support.
A multi-step run needs one rule. Find the first step where the trace diverges from what you expected, and diagnose that step. Later steps are usually consequences. Say the firm's agent wrote the invented slate price at step nine because the tool returned an empty string at step three. That is a tool-contract defect, and debugging step nine fixes the wrong thing with great confidence.
Reproduce, change one variable at a time, and prove the fix against a fixed set of cases. Anthropic's eval guidance backs the discipline: "Design evals that mirror your real-world task distribution. Don't forget to factor in edge cases!", with automated grading wherever you can get it. For the roofing firm, that set would include the oversized quote request and the slate item.
In the exam
Domain 4 is titled Eval, Testing, and Debugging. Its scope comes from the one sub-skill underneath it, "Debugging and error handling techniques for Claude applications", which includes four parts.
- "error type identification"
- "recovery strategy selection"
- "trace analysis to identify failure modes"
- "problem origin isolation between the integration layer and model output"
Eval appears here only in service of diagnosis. The practice questions here never test eval methodology for its own sake.
The type strings are exact, and the exam tests them exactly. In the practice diagnosis questions, the deciding detail is always in the stem, and the question tests whether you read it. Every wrong answer in this sub-skill is a recovery aimed at a different class from the one in the stem.
Sourcing Notes
The docs prescribe no fix for model_context_window_exceeded. Our own reading is that the remedy is context budgeting.
Dated current-state note (September 2026): output_format has moved to output_config.format, and beta headers are no longer required. The raw output_format parameter is deprecated. It works only with the structured-outputs-2025-11-13 beta header, and without that header the API returns a 400.
Promoting each failing input into a permanent regression case is our engineering practice rather than a documented Anthropic rule. It is what makes the domain's title cohere.
Exam Traps
Treating a transient fault as deterministic or the reverse, and inventing plausible-sounding error type strings along the way.
The tells are in the stem: ‘fails identically every time’ is the deterministic class no retry can fix, while ‘one request in twenty, clustered under load’ is the retryable class. The documented type strings are exactly invalid_request_error, authentication_error, billing_error, permission_error, not_found_error, conflict_error, request_too_large, rate_limit_error, api_error, timeout_error and overloaded_error, and a near-miss alternative is a distractor by construction.
Reading a truncated answer as a model-quality problem and answering it with a sterner prompt or a larger model.
A response that stops mid-sentence at a consistent length is a 200 with stop_reason max_tokens, a configuration fault whose fix is the token budget or the task scope. Its neighbour model_context_window_exceeded means generation stopped at the model's context-window limit rather than your cap, so treat the response as truncated. The docs prescribe no fix, and context budgeting is the usual remedy. Neither is repaired by prompt wording or a stronger model.
Assuming a 200 means the call is safe, so a try/except around the call is complete error handling.
refusal is a stop_reason on a normal 200 rather than an error, and a streaming response can open with a 200 and then carry an error frame mid-stream. A client that only wraps the call reports an overload as a short success and a refusal as an answer.
Debugging the last step of a multi-step run, because that is where the visible symptom appeared.
The failure is at the first divergence from expectation; later steps are consequences. An agent that fabricated or looped late in a run has usually been handed an unusable tool result early in it, which is a tool-contract defect rather than a model-quality one.
Practice Scenario
A swing-bridge authority's structures unit runs a Claude service that turns each span's overnight instrument log into a plain-language morning duty notice for bridge crews. Three symptoms land in one shift: roughly one request in twenty fails with a rate-limit error, clustered when the overnight backlog is flushed at 05:00; one span's logs fail immediately with the identical error on every attempt, at any hour; and a third span's notices return successfully but stop mid-sentence at about the same length every run. The duty engineer proposes a single fix for all three: retry every failure three times, and move the service to a larger model. Which assessment is correct?
Build Exercise
Instrument One Call, Then Break It Four Ways
What you'll learn
- How to capture the six discriminators that make a failure reproducible
- How to tell a deterministic request fault from a transient one using evidence rather than intuition
- How to read stop_reason as a diagnostic on a successful response
- How to match a recovery to an error class and prove the fix against a fixed set of cases
- Instrument one working Claude call in Python or TypeScript so it logs the request id, the HTTP status, stop_reason, usage and the raw content on every call, then break it with a revoked or malformed key and record what came back
Why: Those fields are the discriminators every later step depends on, and an auth fault is the cleanest deterministic failure there is to see them against.
You should see: 401 with error.type authentication_error, arriving as AuthenticationError, identical on every attempt, alongside a clean log line from the successful runs.
- Break it with a rate-limit burst by firing a tight loop of concurrent requests, capture the 429 with its retry-after and anthropic-ratelimit-*-remaining headers, then write capped exponential backoff with jitter that honours retry-after
Why: This is the retryable class and the one recovery with real design content in it: the jitter is what stops a drained backlog re-synchronising into the next burst.
You should see: The burst draining steadily rather than re-clustering, with waits tracking retry-after wherever the header is present.
- Break it with truncation by setting max_tokens far below what the task needs, confirm the call still returns 200, and read stop_reason off the response
Why: Watching a failure arrive as a success is the fastest cure for the assumption that a 200 means the call is done.
You should see: HTTP 200, stop_reason max_tokens, and output_tokens sitting exactly on your cap.
- Break it with a schema-invalid response by asking for JSON your parser rejects, re-ask once with the specific validation errors attached, then re-run with output_config.format carrying a json_schema
Why: Re-asking blind against re-asking with the errors attached is the difference between hoping and diagnosing, and the constrained-decoding path shows what a structural fix looks like.
You should see: A clear verdict on whether the schema, the prompt or your parser was at fault, plus a 400 with details if the schema uses an unsupported feature.
- For each of the four breaks, record the layer, the error class, the recovery you chose and why; then turn each failing input into a regression case, change one variable at a time, and prove all four fixes hold against the same fixed set
Why: The four-row table is the whole sub-skill on one page, and running the fixed set afterwards is what makes any of the fixes attributable rather than merely plausible.
You should see: A four-row table plus a rerunnable case file that passes on all four inputs.
Sources
- Claude API errors — Anthropic, the status-to-type map, the spend-limit 400 and spend-cap 429, the error body shape, request size limits, request ids, the warning about errors after a 200, and the long-requests guidance that points a 504 at streaming
- Stop reasons and fallback — Anthropic, every stop_reason value, refusal as a normal 200, empty end_turn replies after tool results, and the stop-reasons-against-errors distinction
- Refusals and fallback — Anthropic, server-side fallback, the SDK refusal-fallback middleware, discarding partial output and counting refusals as their own signal
- Server tools — Anthropic, continuing a pause_turn with the same tools and capping the continuations
- Streaming messages — Anthropic, the error event frame, unknown event types, and the version-split resume strategy
- Rate limits — Anthropic, retry-after, the anthropic-ratelimit headers, the token bucket, acceleration limits, the tier spend cap and self-set spend limits
- Python SDK — Anthropic, the typed exception table, the retried set, max_retries and the 10-minute timeout default
- TypeScript SDK — Anthropic, the same exception hierarchy and maxRetries in the Node client
- Handle tool calls — Anthropic, the tool_result contract, is_error, and instructive tool error messages
- Structured outputs — Anthropic, output_config.format, strict tool use, and the 400 on unsupported schema features
- Define success criteria and build evaluations — Anthropic, task-specific evals, automated grading and using a different model to grade
- Reduce hallucinations — Anthropic, letting Claude say it does not know, quote extraction and citation verification
- Claude Certified Developer Foundations Exam Guide — Domain 4, Debugging and Error Handling — Anthropic