BeClaude
GuideBeginnerAgents2026-05-13

Mastering Claude API Stop Reasons: Build Robust Applications That Handle Every Response Type

Learn how to interpret and handle Claude's stop_reason field—end_turn, max_tokens, stop_sequence, and tool_use—to build reliable, production-ready applications with the Claude API.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, max_tokens, stop_sequence, and tool_use—and shows you how to handle each case with practical code examples to prevent empty responses, manage tool loops, and build robust applications.

Claude APIstop_reasonerror handlingtool usebest practices

Mastering Claude API Stop Reasons: Build Robust Applications That Handle Every Response Type

When you send a request to the Claude API, the response includes a stop_reason field that tells you why the model stopped generating. Understanding these values is essential for building reliable applications—whether you're creating a simple chatbot, a complex agent with tool use, or a streaming interface.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle each case in Python and TypeScript
  • How to prevent and fix empty responses
  • Best practices for production applications

What Is the stop_reason Field?

The stop_reason field is part of every successful Messages API response. Unlike errors (which indicate a failure), stop_reason tells you why Claude successfully completed its response generation.

Here's a typical response structure:

{
  "id": "msg_01234",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Here's the answer to your question..."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 100,
    "output_tokens": 50
  }
}

The Four Stop Reason Values

1. end_turn — Natural Completion

What it means: Claude finished its response naturally. This is the most common stop reason and usually indicates a complete, satisfactory answer. How to handle it: Process the response content directly.
from anthropic import Anthropic

client = Anthropic() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}] )

if response.stop_reason == "end_turn": # Process the complete response print(response.content[0].text)

#### ⚠️ The Empty Response Gotcha

Sometimes Claude returns an empty content array with stop_reason: "end_turn". This typically happens in tool-use scenarios when:

  • You add text blocks immediately after tool results — Claude learns to expect the user to always insert text after tool results, so it ends its turn to follow the pattern.
  • You send Claude's completed response back without adding anything — Claude already decided it's done, so it remains done.
How to prevent empty responses:
# INCORRECT: Adding text immediately after tool_result
messages = [
    {"role": "user", "content": "Calculate the sum of 1234 and 5678"},
    {"role": "assistant", "content": [{
        "type": "tool_use",
        "id": "toolu_123",
        "name": "calculator",
        "input": {"operation": "add", "a": 1234, "b": 5678}
    }]},
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"},
        {"type": "text", "text": "Here's the result"}  # ❌ Don't add text after tool_result
    ]}
]

CORRECT: Send tool results directly without additional text

messages = [ {"role": "user", "content": "Calculate the sum of 1234 and 5678"}, {"role": "assistant", "content": [{ "type": "tool_use", "id": "toolu_123", "name": "calculator", "input": {"operation": "add", "a": 1234, "b": 5678} }]}, {"role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"} # ✅ Just the tool_result, no additional text ]} ]
If you still get empty responses:
def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages
    )
    
    if response.stop_reason == "end_turn" and not response.content:
        # ❌ Don't just retry with the same messages
        # This won't work because Claude already decided it's done
        
        # ✅ Add a continuation prompt in a NEW user message
        messages.append({"role": "user", "content": "Please continue"})
        response = client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    
    return response

2. max_tokens — Token Limit Reached

What it means: Claude hit the max_tokens limit you set. The response may be cut off mid-sentence or mid-thought. How to handle it: Continue the conversation by sending the response back as part of the conversation history.
def handle_max_tokens(client, messages, response):
    if response.stop_reason == "max_tokens":
        # Add Claude's partial response to the conversation
        messages.append({"role": "assistant", "content": response.content})
        # Ask Claude to continue
        messages.append({"role": "user", "content": "Please continue."})
        
        # Get the continuation
        continuation = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
        return continuation
    return response

3. stop_sequence — Custom Stop Sequence Triggered

What it means: Claude encountered a custom stop sequence you defined in your request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter. How to handle it: Extract the content up to the stop sequence and process it as needed.
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "List three colors"}],
    stop_sequences=["\n"]  # Stop at the first newline
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") print(f"Content: {response.content[0].text}")

4. tool_use — Tool Call Requested

What it means: Claude wants to call a tool. The response content will contain one or more tool_use blocks. This is the foundation for building agents. How to handle it: Execute the tool, then send the results back.
def handle_tool_use(client, messages, response):
    if response.stop_reason == "tool_use":
        # Add Claude's response (with tool_use blocks) to history
        messages.append({"role": "assistant", "content": response.content})
        
        # Process each tool call
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                })
        
        # Send tool results back
        messages.append({"role": "user", "content": tool_results})
        
        # Get Claude's response after tool use
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response

Building a Complete Response Handler

Here's a robust handler that manages all stop reasons in a loop:

def get_complete_response(client, messages, max_iterations=10):
    """
    Handle all stop reasons and continue until we get a complete response.
    """
    for _ in range(max_iterations):
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            if response.content:
                return response  # Complete, non-empty response
            else:
                # Empty response - add continuation prompt
                messages.append({"role": "user", "content": "Please continue."})
                continue
        
        elif response.stop_reason == "max_tokens":
            # Add partial response and ask to continue
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
            continue
        
        elif response.stop_reason == "stop_sequence":
            # Custom stop sequence reached - return as-is
            return response
        
        elif response.stop_reason == "tool_use":
            # Handle tool calls
            messages.append({"role": "assistant", "content": response.content})
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    })
            messages.append({"role": "user", "content": tool_results})
            continue
    
    raise Exception("Max iterations reached without complete response")

Best Practices for Production

  • Always check stop_reason — Never assume a response is complete. Always inspect the stop_reason field.
  • Handle empty end_turn responses gracefully — Implement the continuation pattern shown above.
  • Set reasonable max_tokens — Too low a limit will cause frequent max_tokens stops, increasing latency and cost.
  • Log stop_reason for debugging — Track which stop reasons occur most often in your application to fine-tune your implementation.
  • Use stop_sequences for structured output — When you need Claude to stop at a specific delimiter (e.g., JSON closing brace), define custom stop sequences.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), max_tokens (token limit hit), stop_sequence (custom delimiter reached), and tool_use (tool call requested).
  • Empty end_turn responses occur in tool-use scenarios when you add text after tool_result blocks—send tool results directly without extra text.
  • For max_tokens, continue the conversation by adding Claude's partial response to the message history and asking it to continue.
  • For tool_use, execute the tool and send results back in a tool_result block to continue the conversation.
  • Build a loop handler that processes all stop reasons to ensure your application always gets a complete, useful response.