Handle API Errors
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:
| Attribute | Contents |
|---|---|
status_code | The HTTP status, such as 400 or 529 |
message | A short human-readable summary |
body | The parsed JSON error body, when the server sent one |
response | The full HTTP response, including headers |
request_id | The 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:
| Status | Class | Typical error.type | Meaning |
|---|---|---|---|
| 400 | BadRequestError | invalid_request_error | The request is malformed or breaks a rule |
| 401 | AuthenticationError | authentication_error | The API key is missing or invalid |
| 403 | PermissionDeniedError | permission_error | The key is valid but not allowed to do this |
| 404 | NotFoundError | not_found_error | The resource, such as a file or batch, does not exist |
| 409 | ConflictError | conflict_error | The request conflicts with the current state |
| 413 | APIStatusError | request_too_large | The request exceeds the size limit |
| 422 | UnprocessableEntityError | varies | The request is well formed but cannot be processed |
| 429 | RateLimitError | rate_limit_error | Too many requests or too many tokens for a period |
| 500 and above | InternalServerError | api_error, overloaded_error | A 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
fake_status_error()builds a realhttpx.Responseand 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.- The class is chosen by status. A
529has no dedicated class, but it is above 500, so it becomes anInternalServerError. The SDK's own dispatch works the same way. describe()checks timeouts first, becauseAPITimeoutErroris a child ofAPIConnectionError. The order in the function mirrors the order a handler must use.- The
529error keeps itsoverloaded_errortype, which is more specific than the status alone. Some handlers treat overload differently from a generic server error, so keeping the type matters. - 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:
| Category | Classes | Typical decision |
|---|---|---|
| Fix the request | BadRequestError, UnprocessableEntityError, request_too_large | Do not retry unchanged. Correct the input. |
| Fix the configuration | AuthenticationError, PermissionDeniedError | Do not retry. Check the key and the workspace. |
| Wait and retry | RateLimitError, InternalServerError, ConflictError, timeouts, connection errors | Retry with backoff, as Lessons 142–146 explain |
| Fix the data reference | NotFoundError | Check 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.