Handle Network Errors

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 145 of 200

When the Request Never Arrives

Most errors from the service come back as a status code, which tells the caller what happened. Network errors are different. They happen between the caller and the service, so the caller may never learn whether the request reached the server, was processed, or was lost. The SDK reports them as APIConnectionError or APITimeoutError (Lesson 141), and those classes carry no status code, no body, and no request ID.

That silence is what makes network errors hard to reason about. A status code tells you the server's decision. A network error tells you only that no decision reached you. This lesson explains the kinds of network failure, how to tell them apart from the underlying exceptions, what each one means for the work in progress, and how to recover partial answers when a response is cut off partway through.


Where the Failure Happens

A request crosses several layers, and a failure can happen at any of them:

application ──► DNS lookup ──► TCP connect ──► TLS handshake ──► send request ──► receive response
                    │              │               │                  │                  │
                 "cannot find    "refused or     "certificate or    "connection        "connection
                  the host"       unreachable"    handshake fails"   reset"             dropped mid-reply"

The SDK does not expose each of these as its own exception. It wraps the lower-level errors from the HTTP library, and keeps the original as the error's __cause__. That chain is the most useful diagnostic information a network error carries.


Reading the Cause Chain

When the SDK raises APIConnectionError, Python stores the underlying exception in error.__cause__. Checking that attribute tells you which layer failed. The httpx exception types map to the layers above:

CauseTypical meaningWhat it suggests
httpx.ConnectErrorThe host could not be reached: lookup failure, refused connection, or unreachable networkCheck DNS, proxy, firewall, and general connectivity
httpx.ReadError, httpx.RemoteProtocolErrorThe connection closed before the response finishedA transient drop, or a server that restarted mid-request
httpx.TimeoutException subclassesA phase exceeded its timeout (Lesson 144)The server or network is slow, or the limit is too short
NoneNo further detail was recordedTreat as a generic network problem

The cause chain turns a vague "connection error" into a specific question. A ConnectError on every request from one machine suggests a local network problem, while the same error from every machine suggests an outage elsewhere. The classifier below encodes those checks:

"""network_errors.py: classify network failures from the SDK's cause chain."""

import httpx
from anthropic import APIConnectionError, APITimeoutError

REQUEST = httpx.Request("POST", "https://api.anthropic.com/v1/messages")


def classify_network(error: Exception) -> str:
    cause = error.__cause__
    if isinstance(error, APITimeoutError):
        return "timeout: no answer within the time limit"
    if isinstance(cause, httpx.ConnectError):
        return "unreachable: lookup, routing, or connection refused"
    if isinstance(cause, (httpx.ReadError, httpx.RemoteProtocolError)):
        return "dropped: the connection closed before the response finished"
    if cause is not None:
        return f"network problem: {type(cause).__name__}"
    return "network problem: no further detail"


def with_cause(error: Exception, cause: Exception) -> Exception:
    error.__cause__ = cause
    return error


if __name__ == "__main__":
    print(classify_network(APITimeoutError(request=REQUEST)))
    print(classify_network(with_cause(APIConnectionError(request=REQUEST),
                                      httpx.ConnectError("name resolution failed"))))
    print(classify_network(with_cause(APIConnectionError(request=REQUEST),
                                      httpx.RemoteProtocolError("server disconnected"))))
    print(classify_network(APIConnectionError(request=REQUEST)))

Output:

timeout: no answer within the time limit
unreachable: lookup, routing, or connection refused
dropped: the connection closed before the response finished
network problem: no further detail

How It Works

  1. The timeout check comes first, because APITimeoutError is a subclass of APIConnectionError. Checking the parent first would hide the more specific class.
  2. The cause is read from __cause__, the standard Python link to the exception that led to the current one. The SDK sets it when it wraps the HTTP library's error.
  3. Each httpx type maps to one diagnosis. Connection refusals and lookup failures point to reachability. Closed connections point to something interrupting the reply.
  4. with_cause() builds the cause chain for tests, so each diagnosis can be checked without a real network.
  5. Unknown causes still produce a message, so a log never ends up with an empty explanation.

What Happened to the Work

A network error leaves one question open: did the server process the request? The answer changes what the caller should do:

SituationServer processed it?Retry safety
Connection refused, or lookup failedNo, the request never arrivedSafe to retry
Connection dropped before any data arrivedUnknown, possibly notRetry, and accept the small chance of a duplicate
Connection dropped after part of the answer arrivedYes, the server was generatingKeep the partial answer, and continue rather than repeat
Timeout on a non-streamed requestPossibly yes, and the answer may be lostRetry only if the operation is safe to repeat (Lesson 143)

The middle rows are where duplicates come from. If the server finished the work and only the reply was lost, a retry does the work again. That is acceptable for an answer that is only read, and dangerous for an action that changes state. Lesson 143's idempotency rules decide which case applies.


Recovering a Partial Answer

Streaming makes partial answers common. A stream delivers text as it is generated, so a drop in the middle leaves the caller with the beginning of the answer. Discarding that text and starting over wastes the tokens already produced, and risks a different answer. A better approach keeps what arrived and asks the model to continue from it.

The helper below collects a stream and reports whether it finished. When it did not, the partial text is returned along with a flag, so the caller can decide how to proceed:

class FlakyStream:
    """Yields chunks, then fails partway through, the way a dropped connection does."""

    def __init__(self, chunks: list[str], fail_after: int):
        self.chunks = chunks
        self.fail_after = fail_after

    def __iter__(self):
        for index, chunk in enumerate(self.chunks):
            if index == self.fail_after:
                raise APIConnectionError(request=REQUEST)
            yield chunk


def collect(stream) -> tuple[str, bool]:
    received = []
    try:
        for chunk in stream:
            received.append(chunk)
        return "".join(received), True
    except APIConnectionError:
        return "".join(received), False


if __name__ == "__main__":
    text, complete = collect(FlakyStream(["The answer ", "is 42", "."], fail_after=2))
    print(repr(text), "complete:", complete)

Output:

'The answer is 42' complete: False

How It Works

  1. The stream yields chunks until the failure point, then raises the same error a dropped connection would produce.
  2. collect() keeps every chunk that arrived, in order, in the list. The exception is caught only to record that the stream did not finish.
  3. The return value carries both pieces of information: the text received, and whether it is complete. Callers cannot accidentally treat a partial answer as a finished one, because the flag must be checked.
  4. The example stops at two chunks, which is why the text reads as an unfinished sentence, and the flag is False.

Continuing a Cut-Off Answer

Once the partial text is safe, the next step is to ask Claude to continue. On current models, prefilling, which means ending the messages with a partial assistant turn, is not supported. The continuation must therefore be phrased as a request: the question, the partial answer, and an instruction to carry on from where the answer stopped.

def continuation_messages(question: str, partial: str) -> list[dict]:
    return [
        {"role": "user", "content": question},
        {"role": "user", "content":
            f"Your answer was cut off. Continue from exactly where it stopped, without "
            f"repeating what you already wrote.\n\nAnswer so far:\n{partial}"},
    ]


messages = continuation_messages("Explain what a token is.", "A token is a small piece of")
for message in messages:
    print(message["role"], "->", message["content"].split(".")[0])

Output:

user -> Explain what a token is
user -> Your answer was cut off

How It Works

  1. The original question is kept, so the continuation has the same context as the first attempt.
  2. The partial answer is quoted in the instruction, so Claude can see exactly where it stopped. The instruction tells it not to repeat what was already written.
  3. The example uses two user messages, to keep the original question distinct from the instruction. Because consecutive messages with the same role are a sensitive detail of the API's message rules, check the current message-format documentation, and merge the two into one message if your SDK version requires it. A single combined message carries the same content.
  4. Nothing in the request puts words in Claude's mouth. The continuation is driven by an explicit instruction, which current models follow reliably.

Note: a continuation is a new request, so it is billed, and it may not reproduce the same wording the first attempt would have produced. Keep the partial answer alongside the continuation, so the final text can be assembled from both.


Diagnosing Persistent Network Problems

Transient failures pass, and a retry layer covers them (Lesson 143). A problem that persists across many attempts calls for diagnosis. Two checks answer most questions: whether the failure is specific to one machine, and whether the status page reports an incident. A failure on every machine points to the service, and a failure on one machine points to that machine's network, proxy, or certificates.

A proxy that rewrites or blocks encrypted traffic can also cause TLS errors that look like connection errors, and the cause chain usually shows the difference. Certificate problems appear as handshake failures before any request is sent, while a dropped response appears only after the request was sent.


Summary

Network errors carry no status code, so the cause chain is the main evidence. Each layer of the request can fail, and each failure leaves a different question about whether the server processed the work. Retries cover the transient cases, and idempotency decides whether repeating is safe. When a stream is cut off, the text already received is valuable, and a continuation request can finish the answer without discarding the tokens already paid for.

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 Network Errors and get answers drawn from it.

Signed-in readers only.