Handle File Errors

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

Files Add New Ways to Fail

Requests with images, PDFs, and uploaded files fail in ways that plain text requests do not. A file can be the wrong type, too large, corrupt, or no longer available. A stored file ID can expire or be deleted. Each of these problems needs a different response. Some are fixed by the user, some by re-uploading, and some by waiting.

Lessons 43, 57, 77, and 105 covered stop reasons, stream errors, async errors, and invalid output. This lesson covers errors specific to files, and shows how to turn them into the right action.


Errors by Cause

CauseTypical signalRight response
Unsupported or corrupt file400 (BadRequestError)Tell the user; do not retry
File or request too large413 (APIStatusError with status 413)Resize or split; do not retry as is
Stored file ID missing or deleted404 (NotFoundError)Upload the file again, then retry once
Rate limit429 (RateLimitError)Wait and retry
Service problem5xx (InternalServerError)Wait and retry
Network problemAPIConnectionErrorWait and retry
Refusal or truncationstop_reasonHandle as in Lesson 43

The key distinction is between permanent problems, where retrying the same request cannot succeed, and temporary problems, where it can. Retrying a 413 produces another 413, and wastes time and tokens.


Check Before You Send

Most file errors are predictable, so catch them locally whenever possible. A validation function checks the content, the type, and a local size policy before any request is made (Lessons 111–114):

MAX_FILE_BYTES = 20 * 1024 * 1024      # Local policy, not an API limit


def validate_upload(name: str, data: bytes) -> list[str]:
    problems = []
    if not data:
        problems.append("the file is empty")
    if len(data) > MAX_FILE_BYTES:
        problems.append(f"{name} is {len(data) // (1024 * 1024)} MB; the limit is 20 MB")
    if not (data.startswith(b"%PDF-") or detect_media_type(data)):
        problems.append("unsupported file type; use PDF, JPEG, PNG, GIF, or WebP")
    return problems

A local check is free, instant, and gives a message the user can act on. An API error is none of these, so local checks should catch everything they can.


Translating Errors for Users

Error messages from the API are written for developers. Users need a message that says what happened and what to do. The mapping below keeps the technical details in logs, and returns only what the user needs:

import anthropic


def explain_file_error(error: Exception) -> tuple[str, bool]:
    """Return (message for the user, whether trying again could help)."""
    if isinstance(error, anthropic.APIConnectionError):
        return "The connection failed. Please try again.", True
    if isinstance(error, anthropic.RateLimitError):
        return "The service is busy. Please wait a moment and try again.", True
    if isinstance(error, anthropic.InternalServerError):
        return "The service had a problem. Please try again shortly.", True
    if isinstance(error, anthropic.NotFoundError):
        return "The stored file is no longer available. It will be uploaded again.", False
    if isinstance(error, anthropic.APIStatusError):
        if error.status_code == 413:
            return "This file or request is too large. Try a smaller file or fewer pages.", False
        if error.status_code == 400:
            return "The file could not be read. Check that it is a valid, uncorrupted file.", False
    return "Something went wrong with this file. Please try again.", False

The order of the checks matters. RateLimitError, InternalServerError, and NotFoundError are all subclasses of APIStatusError, so the specific cases must come before the general one.


Practical Example: Asking About a File, Safely

This example combines the pieces. It validates the upload, sends the file, re-uploads once if the stored ID has expired, retries temporary failures with backoff, and returns either an answer or a clear message.

"""safe_file_question.py: ask about a file, with validation, re-upload, and bounded retries."""

import time

import anthropic
from anthropic import Anthropic

from media_utils import detect_media_type

client = Anthropic()
MODEL = "claude-opus-5-5"
MAX_FILE_BYTES = 20 * 1024 * 1024
ATTEMPTS = 3


def validate_upload(name: str, data: bytes) -> list[str]:
    problems = []
    if not data:
        problems.append("the file is empty")
    if len(data) > MAX_FILE_BYTES:
        problems.append(f"{name} is {len(data) // (1024 * 1024)} MB; the limit is 20 MB")
    if not (data.startswith(b"%PDF-") or detect_media_type(data)):
        problems.append("unsupported file type; use PDF, JPEG, PNG, GIF, or WebP")
    return problems


def media_type_of(data: bytes) -> str | None:
    return "application/pdf" if data.startswith(b"%PDF-") else detect_media_type(data)


def explain_file_error(error: Exception) -> tuple[str, bool]:
    if isinstance(error, anthropic.APIConnectionError):
        return "The connection failed. Please try again.", True
    if isinstance(error, anthropic.RateLimitError):
        return "The service is busy. Please wait a moment and try again.", True
    if isinstance(error, anthropic.InternalServerError):
        return "The service had a problem. Please try again shortly.", True
    if isinstance(error, anthropic.NotFoundError):
        return "The stored file is no longer available. It will be uploaded again.", False
    if isinstance(error, anthropic.APIStatusError):
        if error.status_code == 413:
            return "This file or request is too large. Try a smaller file or fewer pages.", False
        if error.status_code == 400:
            return "The file could not be read. Check that it is a valid, uncorrupted file.", False
    return "Something went wrong with this file. Please try again.", False


def ask(data: bytes, file_id: str, question: str) -> str:
    block_type = "document" if data.startswith(b"%PDF-") else "image"
    message = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": [
            {"type": block_type, "source": {"type": "file", "file_id": file_id}},
            {"type": "text", "text": question},
        ]}],
    )
    return "".join(block.text for block in message.content if block.type == "text").strip()


def ask_about_file(name: str, data: bytes, question: str) -> str:
    problems = validate_upload(name, data)
    if problems:
        raise ValueError("; ".join(problems))

    file_id = None
    for attempt in range(1, ATTEMPTS + 1):
        try:
            if file_id is None:
                file_id = client.files.upload(file=(name, data, media_type_of(data))).id
            return ask(data, file_id, question)
        except anthropic.NotFoundError:
            file_id = None                        # The stored file is gone: upload again next time
        except (anthropic.RateLimitError, anthropic.InternalServerError, anthropic.APIConnectionError) as error:
            message, _ = explain_file_error(error)
            if attempt == ATTEMPTS:
                raise RuntimeError(message) from error
            time.sleep(2 ** attempt)              # Back off before the next attempt
        except anthropic.APIError as error:
            message, _ = explain_file_error(error)
            raise RuntimeError(message) from error
    raise RuntimeError("The file could not be used. Please try again.")


if __name__ == "__main__":
    # Offline demonstrations: no API calls
    print(validate_upload("notes.txt", b"plain text, not an image"))
    print(validate_upload("empty.pdf", b""))

    request = anthropic.httpx.Request("POST", "https://api.anthropic.com/v1/messages")
    samples = [
        ("HTTP 413", anthropic.APIStatusError("too big", response=anthropic.httpx.Response(413, request=request), body=None)),
        ("rate limit", anthropic.RateLimitError("busy", response=anthropic.httpx.Response(429, request=request), body=None)),
        ("missing file", anthropic.NotFoundError("gone", response=anthropic.httpx.Response(404, request=request), body=None)),
        ("network", anthropic.APIConnectionError(request=request)),
    ]
    for label, error in samples:
        message, retry = explain_file_error(error)
        print(f"{label} | retry={retry} | {message}")

Output:

['unsupported file type; use PDF, JPEG, PNG, GIF, or WebP']
['the file is empty', 'unsupported file type; use PDF, JPEG, PNG, GIF, or WebP']
HTTP 413 | retry=False | This file or request is too large. Try a smaller file or fewer pages.
rate limit | retry=True | The service is busy. Please wait a moment and try again.
missing file | retry=False | The stored file is no longer available. It will be uploaded again.
network | retry=True | The connection failed. Please try again.

The anthropic.httpx reference works because the SDK imports httpx. In your own code, import httpx is clearer.

How It Works

  1. validate_upload() catches empty, oversized, and unsupported files before any request is made. The output shows that an empty file produces two problems, since it is both empty and unidentifiable.
  2. explain_file_error() separates the error types that users can understand, and records whether a retry could help. The retry flag is what the caller uses to decide.
  3. ask_about_file() handles each case deliberately. A missing stored file triggers a fresh upload on the next attempt. Temporary failures wait with exponential backoff, for at most three attempts. Permanent failures stop immediately.
  4. The except clauses are ordered from specific to general, so NotFoundError and the temporary errors are handled before the general APIError branch.
  5. Technical details are kept out of the messages. The user sees the mapped text, and the original error is chained with from error, so logs can still show the full cause (Lesson 49).

Common Mistakes

Retrying Permanent Errors

What happens: a 413 or 400 is retried several times, adding delay and cost, and the result never changes.

Fix: retry only temporary errors, and fix the cause for the rest.

Showing Raw API Messages to Users

What happens: messages such as "invalid_request_error" confuse people and reveal internal details.

Fix: translate errors into actionable messages, and log the originals.

Checking Only After Sending

What happens: every invalid file costs a request, and users wait for a failure they could have been told about immediately.

Fix: validate locally first.

Ignoring Missing Stored Files

What happens: conversations that reference an old file ID fail permanently.

Fix: catch NotFoundError, re-upload, and retry once (Lesson 116).

Retrying Without Backoff

What happens: a busy service receives more requests from every retry, and the problem grows.

Fix: wait longer after each failed attempt, and limit the number of attempts.


Best Practices

  • Validate files before sending, using the content, not the name.
  • Classify errors as temporary or permanent before deciding on a retry.
  • Order exception handlers from specific to general.
  • Translate errors into messages users can act on.
  • Log the original error with its request ID (Lesson 49).
  • Re-upload when a stored file is missing, and retry only once.
  • Cap attempts and back off between them.

Exercise

  1. Add a check to validate_upload() that rejects images whose width or height exceeds 8000 pixels, using Image.open() from Lesson 113.
  2. Add a message for 400 errors that tells the user to check the file type, and a test that a BadRequestError maps to it.
  3. Write a function that returns the number of attempts actually made, so that tests can confirm that permanent errors are not retried.
  4. Explain why explain_file_error() checks NotFoundError before the general APIStatusError branch.
Show answers for steps 3 and 4

Step 3: count attempts inside the loop and return the count with the result, or include it in the exception message:

def ask_counting(data: bytes, question: str) -> tuple[str, int]:
    attempts_made = 0
    for attempt in range(1, ATTEMPTS + 1):
        attempts_made = attempt
        try:
            ...  # the same logic as ask_about_file, returning the answer on success
        except anthropic.APIError:
            ...
    return answer, attempts_made

A test can then send a 413 through a fake client and assert that attempts_made == 1. Testing the retry count is the most direct way to verify that permanent errors are not retried, and it is the kind of test Lesson 110 recommends.

Step 4: NotFoundError is a subclass of APIStatusError, which means it matches the general branch too. If the general branch came first, the 404 would be reported as a generic status error, and the specific advice (re-upload and try again) would never appear. Specific classes must be checked before their parent classes, in every exception handler.

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

Signed-in readers only.