BeClaude
GuideBeginnerAgents2026-05-14

Mastering Claude's Stop Reasons: Build Robust API Applications

Learn how to interpret and handle Claude API stop_reason values including end_turn, tool_use, max_tokens, and stop_sequence. Includes code examples and troubleshooting for empty responses.

Quick Answer

This guide explains Claude's four stop_reason values (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each one in your application. You'll learn to detect empty responses, manage tool calls, handle truncation, and build robust retry logic that gracefully handles all response scenarios.

Messages APIstop_reasonerror handlingtool useAPI best practices

Mastering Claude's Stop Reasons: Build Robust API Applications

When you send a request to the Claude API, you expect a response. But what happens when Claude finishes generating? How do you know if it completed naturally, needs to use a tool, or was cut short? The answer lies in a single field: stop_reason.

Understanding stop_reason is essential for building production-ready applications that gracefully handle every possible response scenario. This guide covers all four stop reasons, common pitfalls, and practical code patterns to make your Claude integration bulletproof.

What Is stop_reason?

Every successful response from the Claude Messages API includes a stop_reason field. Unlike errors (which indicate something went wrong processing your request), stop_reason tells you why Claude stopped generating content.

Here's what a typical response looks like:

{
  "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 stop_reason field can contain one of four values: end_turn, tool_use, max_tokens, or stop_sequence. Each requires a different handling strategy.

The Four Stop Reasons

1. end_turn – Natural Completion

This is the most common stop reason. Claude finished its response naturally and has nothing more to say. It's the ideal outcome for most requests.

How to handle it:
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)

2. tool_use – Claude Wants to Use a Tool

When you've provided tools to Claude, it may decide to invoke one. The response will contain a tool_use content block instead of (or in addition to) text. Your application must handle this by executing the tool and sending the result back.

How to handle it:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "get_weather",
        "description": "Get current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string"}
            },
            "required": ["location"]
        }
    }],
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)

if response.stop_reason == "tool_use": # Extract the tool call from content for block in response.content: if block.type == "tool_use": tool_name = block.name tool_input = block.input # Execute the tool and send result back result = execute_tool(tool_name, tool_input) # Continue the conversation with the tool result

3. max_tokens – Response Was Truncated

Claude hit the max_tokens limit you set before finishing its response. The content is incomplete. This often happens with long responses or when Claude is generating step-by-step reasoning.

How to handle it:
if response.stop_reason == "max_tokens":
    # The response is truncated - continue the conversation
    messages.append({"role": "assistant", "content": response.content})
    messages.append({"role": "user", "content": "Please continue"})
    
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

4. stop_sequence – Custom Stop Sequence Triggered

You provided a custom stop_sequences parameter in your API request, and Claude encountered one. This is useful for structured outputs where you want Claude to stop at a specific delimiter.

How to handle it:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nEND"],
    messages=[{"role": "user", "content": "List 3 facts about space, then write END"}]
)

if response.stop_reason == "stop_sequence": # The response was cut at the stop sequence print(f"Stopped at sequence: {response.stop_sequence}") # Process the content up to the stop sequence

The Empty Response Problem

A common gotcha: sometimes Claude returns an empty response (2-3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use scenarios.

Why It Happens

Claude learns from patterns in the conversation history. If you consistently add text after tool results, Claude learns to expect that pattern and may end its turn prematurely.

How to Prevent It

Incorrect pattern – adding text after tool_result:
messages = [
    {"role": "user", "content": "Calculate 1234 + 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 do this
    ]}
]
Correct pattern – send tool results directly:
messages = [
    {"role": "user", "content": "Calculate 1234 + 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 result
    ]}
]

Recovery Strategy

If you still get empty responses, use a continuation prompt:

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:
        # Add a continuation prompt in a NEW user message
        messages.append({"role": "user", "content": "Please continue"})
        return client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    
    return response

Building a Complete Handler

Here's a robust pattern that handles all stop reasons:

def handle_claude_response(client, messages, tools=None):
    """
    Complete handler for all Claude stop reasons.
    Returns the final response content.
    """
    max_retries = 3
    retry_count = 0
    
    while retry_count < max_retries:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            messages=messages,
            tools=tools
        )
        
        stop_reason = response.stop_reason
        
        if stop_reason == "end_turn":
            if response.content:
                return response.content  # ✅ Complete response
            else:
                # Empty response - try continuation
                messages.append({"role": "user", "content": "Please continue"})
                retry_count += 1
                continue
        
        elif stop_reason == "tool_use":
            # Handle tool calls
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    messages.append({"role": "assistant", "content": response.content})
                    messages.append({"role": "user", "content": [{
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    }]})
            continue  # Let Claude respond to tool results
        
        elif stop_reason == "max_tokens":
            # Response truncated - continue
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue"})
            retry_count += 1
            continue
        
        elif stop_reason == "stop_sequence":
            # Custom stop - process as needed
            return response.content
    
    raise Exception("Max retries reached for empty/truncated responses")

Best Practices Summary

  • Always check stop_reason – Don't assume the response is complete just because you got a 200 status code.
  • Handle empty end_turn responses – Especially in tool-use workflows, implement the continuation pattern.
  • Increase max_tokens for complex tasks – If you frequently see max_tokens, your limit is too low.
  • Use stop_sequences for structured output – Great for parsing JSON or extracting specific sections.
  • Log stop reasons – Monitor which stop reasons your application encounters to identify patterns and optimize.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), tool_use (needs tool invocation), max_tokens (truncated), and stop_sequence (custom delimiter hit)
  • Empty responses happen with tools: Prevent them by sending tool results without extra text; recover with a "Please continue" prompt
  • max_tokens requires continuation: Always check for this and implement retry logic with a continuation prompt
  • Build a unified handler: Create a single function that routes logic based on stop_reason for cleaner, more maintainable code
  • Monitor and log: Track stop reason frequencies to optimize your max_tokens settings and tool configurations