Handle API Errors

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 141 of 200

Why Errors Need Their Own Lesson

Every request so far has succeeded, and that is the case the examples have shown. In production, requests fail for many reasons: the input is wrong, the account is out of credit, the service is busy, the network drops, or the request simply takes too long. Each of these looks like an exception in Python, but they call for completely different responses. A program that treats every failure the same way will either give up on problems that would clear up by waiting, or keep retrying problems that will never clear up.

This lesson introduces the SDK's error hierarchy, explains what each error tells you, and shows how to read the information the server sends back. Later lessons in this section build on it: retries (143), rate limits (142), timeouts (144), and logging (147).


The Error Hierarchy

The SDK groups its exceptions into a tree. The root is APIError, and everything else descends from it. Two branches matter most:

APIError
├── APIConnectionError          ← no usable response: network or DNS problems
│   └── APITimeoutError         ← the request took too long
└── APIStatusError              ← the server answered with an error status
    ├── BadRequestError         400
    ├── AuthenticationError     401
    ├── PermissionDeniedError   403
    ├── NotFoundError           404
    ├── ConflictError           409
    ├── UnprocessableEntityError 422
    ├── RateLimitError          429
    └── InternalServerError     500 and above

The distinction between the two main branches is the first thing to understand. An APIConnectionError means the request never produced an answer, so there is no status code and no request ID. An APIStatusError means the server replied, and the reply says what went wrong.

APITimeoutError is a subclass of APIConnectionError, which is why timeouts are checked before connection problems in any handler. Checking the parent first would catch the child, and the timeout branch would never run.


What an Status Error Carries

An APIStatusError exposes several useful attributes:

AttributeContents
status_codeThe HTTP status, such as 400 or 529
messageA short human-readable summary
bodyThe parsed JSON error body, when the server sent one
responseThe full HTTP response, including headers
request_idThe request-id header, which support teams use to find the request

The body follows a documented shape, with a top-level type of "error" and a nested error object that names the kind of failure:

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "max_tokens: must be at least 1"
  }
}

The status code says which class of problem occurred, and the nested error.type says what kind of problem it is. Two errors with the same status can have different types, and the type is often the more precise signal.


Reading the Error Types

Common status codes and their error types follow a fairly stable pattern, as the documentation describes:

StatusClassTypical error.typeMeaning
400BadRequestErrorinvalid_request_errorThe request is malformed or breaks a rule
401AuthenticationErrorauthentication_errorThe API key is missing or invalid
403PermissionDeniedErrorpermission_errorThe key is valid but not allowed to do this
404NotFoundErrornot_found_errorThe resource, such as a file or batch, does not exist
409ConflictErrorconflict_errorThe request conflicts with the current state
413APIStatusErrorrequest_too_largeThe request exceeds the size limit
422UnprocessableEntityErrorvariesThe request is well formed but cannot be processed
429RateLimitErrorrate_limit_errorToo many requests or too many tokens for a period
500 and aboveInternalServerErrorapi_error, overloaded_errorA problem on the service side

The most important consequence is that the same status can call for different responses. A 429 is temporary, and waiting clears it. A 400 is permanent, and waiting changes nothing. Lesson 143 turns this distinction into code.


Handling Errors in the Right Order

When several handlers are written, they must go from specific to general, because a parent class catches all of its children. The safe order is the child classes first, then the status parent, then the base:

import anthropic

try:
    message = client.messages.create(model="claude-opus-5-5", max_tokens=512, messages=[...])
except anthropic.RateLimitError as error:
    ...                                   # 429: wait, then try again
except anthropic.BadRequestError as error:
    ...                                   # 400: fix the request
except anthropic.APIStatusError as error:
    ...                                   # any other status
except anthropic.APIConnectionError as error:
    ...                                   # no response at all
except anthropic.APIError as error:
    ...                                   # anything else from the SDK

Ordering is more than style. If APIStatusError came before RateLimitError, the rate-limit branch would never execute, because a RateLimitError is an APIStatusError.


Building Errors for Tests

Tests need errors without a network. An SDK exception can be built from a fake HTTP response, which lets a test produce any status and body on demand. The helper below creates the right class for a status and attaches the body and request ID:

"""api_errors.py: build SDK error objects from fake responses, and describe them."""

import httpx
from anthropic import (
    APIConnectionError, APIStatusError, APITimeoutError, AuthenticationError, BadRequestError,
    ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError,
    UnprocessableEntityError,
)

STATUS_CLASSES = {
    400: BadRequestError, 401: AuthenticationError, 403: PermissionDeniedError,
    404: NotFoundError, 409: ConflictError, 422: UnprocessableEntityError, 429: RateLimitError,
}
REQUEST = httpx.Request("POST", "https://api.anthropic.com/v1/messages")


def fake_status_error(status: int, error_type: str, message: str, request_id: str):
    body = {"type": "error", "error": {"type": error_type, "message": message}}
    response = httpx.Response(status, request=REQUEST, json=body,
                              headers={"request-id": request_id})
    if status in STATUS_CLASSES:
        cls = STATUS_CLASSES[status]
    elif status >= 500:
        cls = InternalServerError
    else:
        cls = APIStatusError
    return cls(message, response=response, body=body)


def describe(error) -> str:
    if isinstance(error, APITimeoutError):
        return f"timeout | {error}"
    if isinstance(error, APIConnectionError):
        return f"no response | {error}"
    kind = error.body["error"]["type"] if isinstance(error.body, dict) else "unknown"
    return f"{type(error).__name__} | {kind} | request_id={error.request_id}"


if __name__ == "__main__":
    errors = [
        fake_status_error(400, "invalid_request_error", "max_tokens: must be at least 1", "req_400_demo"),
        fake_status_error(401, "authentication_error", "invalid x-api-key", "req_401_demo"),
        fake_status_error(429, "rate_limit_error", "rate limit reached", "req_429_demo"),
        fake_status_error(529, "overloaded_error", "service is overloaded", "req_529_demo"),
        APITimeoutError(request=REQUEST),
        APIConnectionError(request=REQUEST),
    ]
    for error in errors:
        print(describe(error))

Output:

BadRequestError | invalid_request_error | request_id=req_400_demo
AuthenticationError | authentication_error | request_id=req_401_demo
RateLimitError | rate_limit_error | request_id=req_429_demo
InternalServerError | overloaded_error | request_id=req_529_demo
timeout | Request timed out.
no response | Connection error.

How It Works

  1. fake_status_error() builds a real httpx.Response and passes it to the SDK's exception class. The SDK then reads the request ID and body exactly as it would for a live response, so the test exercises real behavior.
  2. The class is chosen by status. A 529 has no dedicated class, but it is above 500, so it becomes an InternalServerError. The SDK's own dispatch works the same way.
  3. describe() checks timeouts first, because APITimeoutError is a child of APIConnectionError. The order in the function mirrors the order a handler must use.
  4. The 529 error keeps its overloaded_error type, which is more specific than the status alone. Some handlers treat overload differently from a generic server error, so keeping the type matters.
  5. Connection errors have no status, body, or request ID. Their messages are the only description available, which is why they are logged differently in Lesson 147.

What to Do with Each Class

A handler decides, for each class, whether the caller should retry, fix the input, ask for credentials, or report a problem. The categories below are the working basis for the rest of this section:

CategoryClassesTypical decision
Fix the requestBadRequestError, UnprocessableEntityError, request_too_largeDo not retry unchanged. Correct the input.
Fix the configurationAuthenticationError, PermissionDeniedErrorDo not retry. Check the key and the workspace.
Wait and retryRateLimitError, InternalServerError, ConflictError, timeouts, connection errorsRetry with backoff, as Lessons 142–146 explain
Fix the data referenceNotFoundErrorCheck the ID, or create the resource again

These categories are the core of reliable error handling. A program that can name the category of every error can decide what happens next, and a program that cannot will guess.


Summary

The SDK's exceptions form a tree, with APIStatusError for replies and APIConnectionError for silence. Each reply carries a status, a body with a specific type, and a request ID. Handlers must check specific classes before general ones, and the decision after each error depends on whether waiting could help. The next lesson applies this to the most common temporary error, the rate limit.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Handle API Errors and get answers drawn from it.

Signed-in readers only.