Handle Stream Errors

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 57 of 200

Why Streams Need Special Error Handling

A normal request either succeeds or fails as a whole. A stream can fail at two different moments, with different consequences:

When it failsWhat you haveTypical causes
Before the stream startsNothing: no events were receivedInvalid key, invalid request, rate limit, network problem, server error
During the streamA partial response: some text was already shown to the userNetwork interruption, error event mid-stream (such as overloaded_error)

The second case is unique to streaming. The user may already have read half an answer when the error occurs. Good error handling must decide what to do with that partial content: discard it, keep it, or continue from it.


Errors Before the Stream Starts

With client.messages.stream(...), the HTTP request is sent when the with block is entered. Errors in the request itself are raised at that point, as the same exception classes as non-streaming requests (Lesson 1):

import anthropic

try:
    with client.messages.stream(
        model="claude-opus-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello"}],
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
except anthropic.AuthenticationError:
    print("Invalid API key.")
except anthropic.RateLimitError:
    print("Rate limited. Please wait and try again.")
except anthropic.APIConnectionError:
    print("Could not connect to the API.")
except anthropic.APIStatusError as error:
    print(f"API error {error.status_code}.")

The SDK's automatic retries (Lesson 10) apply here. Retryable failures of the initial request, such as connection errors, 429, and 5xx, are retried before an exception reaches your code.


Errors During the Stream

Once events are flowing, two kinds of failure can occur.

Error Events

According to the streaming documentation, the API can occasionally send an error event in the middle of a stream. For example, during periods of high usage, an overloaded_error can arrive, which would normally correspond to an HTTP 529 in a non-streaming request:

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

The SDK turns error events into exceptions raised from the loop you are iterating.

Interrupted Connections

The network connection can also drop mid-stream. Depending on where the failure happens, it can surface as an anthropic.APIConnectionError or as an exception from the underlying HTTP library. Robust code therefore treats unexpected exceptions raised during iteration as interruptions, not only anthropic.APIError.

Mid-Stream Errors Are Not Retried Automatically

Automatic retries cover the initial request. Once part of the response has been delivered, the SDK cannot transparently retry, because your code has already received, and possibly displayed, part of the answer. Your application must decide what to do.


Three Ways to Handle an Interrupted Stream

StrategyWhat happensBest for
Discard and reportShow an error; do not store the partial answer; let the user retryShort answers; simple applications
Retry from the startClear the partial output and send the same request againShort answers, or when the partial output contained thinking or tool calls
Continue from the partial textSend a continuation request and append the restLong text answers, where restarting would waste time and tokens

Continuing on Current Models

The documentation describes a capture-and-resume strategy:

  1. Capture the content received before the error.
  2. Construct a continuation request.
  3. Resume streaming and receive the rest.

For Claude 4.6 and later, step 2 uses a user message that contains the partial response and an instruction to continue, because these models do not support a trailing assistant message (prefill). The documentation suggests a prompt along these lines:

Your previous response was interrupted and ended with [previous_response]. Continue from where you left off.

Limitations: only text can be resumed this way. According to the documentation, tool use and thinking blocks cannot be partially recovered. You can resume from the most recent text block. If a stream fails while a tool call is being generated, restart the request instead.


Retrying Before Any Text Arrived

If the stream fails before any text was received, nothing was shown to the user, and the simplest recovery is to try again after a short delay, with a limit on attempts:

import time

for attempt in range(3):
    try:
        ...  # stream the response
        break
    except anthropic.APIError:
        if attempt == 2:
            raise
        time.sleep(2 ** attempt)   # 1s, then 2s

2 ** attempt creates exponential backoff: each wait is longer than the last, giving a busy service time to recover.


Practical Example: A Resilient Streaming Function

This function streams an answer and recovers from interruptions automatically:

  • If the stream fails before any text arrives, it retries the original request with backoff.
  • If the stream fails after some text has arrived, it sends a continuation request and streams the rest.
  • It stops after a fixed number of attempts, and always tells the caller whether the answer is complete.
"""resilient_stream.py: stream an answer and recover from mid-stream interruptions."""

import time
from dataclasses import dataclass

import anthropic
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-opus-5-5"
MAX_ATTEMPTS = 3
NOT_RETRYABLE = (anthropic.AuthenticationError, anthropic.PermissionDeniedError,
                 anthropic.BadRequestError, anthropic.NotFoundError)


@dataclass
class StreamResult:
    text: str
    complete: bool
    attempts: int
    stop_reason: str | None


def with_continuation(messages: list[dict], partial: str) -> list[dict]:
    """Return a request that asks Claude to continue a partial answer (Claude 4.6+ pattern)."""
    last = messages[-1]
    continuation = (
        f"{last['content']}\n\n"
        "Your previous response was interrupted and ended with:\n"
        f"<previous_response>\n{partial}\n</previous_response>\n"
        "Continue from exactly where you left off. Do not repeat earlier text."
    )
    return messages[:-1] + [{"role": "user", "content": continuation}]


def resilient_stream(messages: list[dict], max_tokens: int = 4096) -> StreamResult:
    received = ""
    request_messages = messages

    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            with client.messages.stream(
                model=MODEL, max_tokens=max_tokens, messages=request_messages
            ) as stream:
                for text in stream.text_stream:
                    received += text
                    print(text, end="", flush=True)
                final = stream.get_final_message()
            return StreamResult(received, final.stop_reason == "end_turn", attempt, final.stop_reason)

        except NOT_RETRYABLE:
            raise
        except Exception as error:
            print(f"\n[interrupted ({type(error).__name__}); attempt {attempt} of {MAX_ATTEMPTS}]")
            if attempt == MAX_ATTEMPTS:
                break
            time.sleep(2 ** (attempt - 1))
            if received:
                request_messages = with_continuation(messages, received)

    return StreamResult(received, False, MAX_ATTEMPTS, None)


if __name__ == "__main__":
    result = resilient_stream(
        [{"role": "user", "content": "Explain how Python's garbage collector works, in about 300 words."}]
    )
    print(f"\n\n[complete={result.complete}, attempts={result.attempts}, stop_reason={result.stop_reason}]")

Example output when the connection drops once:

Python manages memory mainly through reference counting: every object keeps a count of
[interrupted (APIConnectionError); attempt 1 of 3]
how many references point to it, and when that count drops to zero, the object is freed...

[complete=True, attempts=2, stop_reason=end_turn]

How It Works

  1. NOT_RETRYABLE lists errors that retrying cannot fix: an invalid key, missing permissions, an invalid request, or an unknown model. They are re-raised immediately, so they are not hidden behind repeated attempts. Because these classes are listed in an except clause before the general except Exception, Python checks them first.
  2. Every other exception during the stream, whether an API error event, a connection failure, or a timeout, is treated as an interruption.
  3. Backoff: time.sleep(2 ** (attempt - 1)) waits 1 second, then 2 seconds, before the next attempt.
  4. No text received yet: received is empty, so request_messages is still the original request, and the same request is sent again. That is a simple retry.
  5. Some text received: with_continuation() builds a new request. The last user message is extended with the partial answer, inside <previous_response> tags, and an instruction to continue. There is no trailing assistant message, as required on Claude 4.6 and later. The function always builds the continuation from the original messages, together with all text received so far, so a second interruption is handled the same way as the first.
  6. New text is appended to received, so the final text is the original part followed by the continuation.
  7. StreamResult always reports whether the answer is complete. Callers never mistake a partial answer for a finished one.

Limitations to keep in mind:

  • The join point may not be perfect: the continuation can occasionally repeat or skip a few words. For critical content, consider regenerating instead.
  • The continuation only works for text. For requests with tools, or when you need thinking blocks, restart the request instead.
  • In a conversation, store the combined text as one assistant turn (Lesson 25), not the continuation scaffolding.

Common Mistakes

Catching Errors Only Around the with Line

What happens: errors raised during iteration escape the handler.

Fix: put the entire with block, including the loop and get_final_message(), inside the try.

Retrying Errors That Cannot Succeed

What happens: invalid keys or bad requests are retried several times, wasting time.

Fix: re-raise authentication, permission, validation, and not-found errors immediately.

Restarting After Showing Partial Text

What happens: the user sees the beginning of the answer twice.

Fix: continue from the partial text, or clear the displayed output before restarting.

Continuing with a Trailing Assistant Message

What happens: a 400 error on Claude 4.6 and later models, which do not support prefill.

Fix: put the partial text and the continuation instruction in a user message.

Treating Partial Answers as Complete

What happens: truncated content is stored or displayed as final.

Fix: track completeness explicitly, and tell users when an answer is incomplete.


Best Practices

  • Wrap the whole stream (entry, iteration, and final message) in error handling.
  • Separate retryable from non-retryable errors.
  • Retry with exponential backoff and a fixed attempt limit.
  • Continue long text answers with a user-message continuation on current models.
  • Restart when the partial content included tool calls or thinking you need.
  • Always report completeness to callers and users.
  • Log interruptions, with request IDs where available (Lesson 49).

Exercise

  1. Simulate an interruption: in the loop, raise an exception after receiving 200 characters, but only on the first attempt. Confirm that the continuation produces a complete answer.
  2. Change resilient_stream() to restart from the beginning, instead of continuing, when fewer than 100 characters had been received. Remember to reset received in that case.
  3. Add a request_id field to StreamResult, filled from the final message.
  4. When the error is an anthropic.APIStatusError, print its request ID (Lesson 49) in the interruption message.
Show answers for steps 1 and 2

Step 1:

simulated = {"done": False}

# Inside the text loop:
received += text
print(text, end="", flush=True)
if not simulated["done"] and len(received) > 200:
    simulated["done"] = True
    raise RuntimeError("simulated network interruption")

The first attempt fails after about 200 characters, and the second attempt continues from that point.

Step 2: in the except Exception block, replace the continuation logic:

if len(received) < 100:
    print("[restarting from the beginning]")
    received = ""
    request_messages = messages
else:
    request_messages = with_continuation(messages, received)

Restarting is simpler and gives a cleaner answer when little was lost. Resetting received prevents the short partial text from being duplicated in the final result. In an interactive display, also clear or mark the partial text the user already saw.

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

Signed-in readers only.