BeClaude
Guide2026-05-02

Mastering Claude API Stop Reasons: A Practical Guide to Handling end_turn, tool_use, and max_tokens

Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes code examples, empty response fixes, and best practices for robust app development.

Quick Answer

This guide explains Claude API stop_reason values (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each in your code. You'll learn to detect empty responses, manage tool call loops, and build robust conversational flows.

Claude APIstop_reasonerror handlingtool useAPI best practices

Introduction

When you call the Claude API, every successful response includes a stop_reason field. This small but mighty piece of data tells you why Claude stopped generating—whether it finished naturally, wants to use a tool, hit a token limit, or encountered a stop sequence. Misunderstanding these signals can lead to broken conversations, empty responses, or infinite loops.

In this guide, you'll learn exactly what each stop_reason means, how to handle it in Python and TypeScript, and how to avoid common pitfalls like empty responses after tool calls.

The stop_reason Field

The stop_reason field appears in every successful Messages API response. Unlike error codes (which indicate failures), stop_reason tells you why Claude successfully stopped generating. Here's a typical response:

{
  "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
  }
}

Stop Reason Values

end_turn

What it means: Claude finished its response naturally. The model decided it had completed its turn in the conversation. How to handle it: This is the simplest case—just process the response content.
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)

#### Empty Responses with end_turn

Sometimes Claude returns an empty response (2-3 tokens with no content) with stop_reason: "end_turn". This typically happens when Claude interprets that the assistant turn is complete, particularly after tool results.

Common causes:
  • Adding 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)
  • Sending Claude's completed response back without adding anything (Claude already decided it's done, so it will remain 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 after fixing the above:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages
    )
    
    # Check if response is empty
    if response.stop_reason == "end_turn" and not response.content:
        # CORRECT: 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

tool_use

What it means: Claude wants to call one or more tools. The response content will contain tool_use blocks with the tool name and input parameters. How to handle it: You must execute the tool, return the result as a tool_result block, and continue the conversation.
def handle_tool_use(response, messages):
    # Extract tool calls from response
    for block in response.content:
        if block.type == "tool_use":
            tool_name = block.name
            tool_input = block.input
            tool_use_id = block.id
            
            # Execute the tool (your implementation)
            result = execute_tool(tool_name, tool_input)
            
            # Add assistant response and tool result to messages
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": [{
                "type": "tool_result",
                "tool_use_id": tool_use_id,
                "content": str(result)
            }]})
    
    # Continue the conversation
    return client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

max_tokens

What it means: Claude stopped because it reached the max_tokens limit you set. The response may be cut off mid-sentence. How to handle it: You can prompt Claude to continue from where it left off.
def handle_max_tokens(response, messages):
    if response.stop_reason == "max_tokens":
        # Add the partial response to messages
        messages.append({"role": "assistant", "content": response.content})
        # Ask Claude to continue
        messages.append({"role": "user", "content": "Please continue from where you left off."})
        
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=2048,  # Consider increasing the limit
            messages=messages
        )
    return response

stop_sequence

What it means: Claude encountered a custom stop sequence you defined in your API request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter. How to handle it: The stop_sequence field in the response will contain the actual sequence that was matched. You can use this to parse structured content.
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\n---END---"],
    messages=[{"role": "user", "content": "Write a short poem and end with ---END---"}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # The content will be everything before the stop sequence print(response.content[0].text)

Building a Complete Handler

For production applications, you should handle all stop reasons in a single loop:

def process_conversation(client, messages, max_iterations=10):
    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":
            # Conversation complete
            return response.content[0].text if response.content else ""
            
        elif response.stop_reason == "tool_use":
            # Handle tool calls
            messages.append({"role": "assistant", "content": response.content})
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    messages.append({"role": "user", "content": [{
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    }]})
            
        elif response.stop_reason == "max_tokens":
            # Continue generation
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
            
        elif response.stop_reason == "stop_sequence":
            # Custom stop sequence reached
            return response.content[0].text
    
    raise Exception("Max iterations reached without completion")

Best Practices

  • Always check stop_reason before processing content. Don't assume end_turn.
  • Handle empty responses gracefully. Implement the continuation prompt pattern for empty end_turn responses.
  • Set appropriate max_tokens. If you frequently see max_tokens stops, increase the limit or implement continuation logic.
  • Use stop_sequences for structured output. Define clear delimiters when you need Claude to output in a specific format.
  • Log stop_reason for debugging. Track which stop reasons occur in production to fine-tune your prompts and parameters.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), tool_use (wants to call a tool), max_tokens (hit token limit), and stop_sequence (custom delimiter reached).
  • Empty responses with end_turn are usually caused by adding text after tool results—send only tool_result blocks in user messages.
  • Tool use requires a loop: When you get tool_use, execute the tool, return the result, and continue the conversation.
  • max_tokens doesn't mean failure: Simply prompt Claude to continue with a new user message.
  • Always build a handler for all stop reasons to create robust, production-ready applications.