BeClaude
Guide2026-05-03

Mastering Claude API Stop Reasons: A Practical Guide to Handling Response Terminations

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 applications.

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 calls, and build robust conversation loops.

Claude APIstop_reasonerror handlingtool useresponse management

Introduction

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 handling tool calls, managing long conversations, or debugging unexpected empty responses.

Unlike API errors (which indicate a failure), stop_reason is part of every successful response. It's your signal for what to do next: continue the conversation, execute a tool, or present the final answer.

The stop_reason Field

Every successful Messages API response includes a stop_reason field. 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
  }
}

Stop Reason Values

Claude can stop for four distinct reasons. Each requires a different response from your application.

1. end_turn

Meaning: Claude finished its response naturally. The model decided it has completed its turn in the conversation. When it occurs: This is the most common stop reason. It happens after Claude answers a question, provides information, or completes a thought. How to handle: Simply present the response to the user. No further action is needed.
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": print(response.content[0].text)

2. tool_use

Meaning: Claude wants to call a tool (function) you've provided. The response content will include one or more tool_use blocks. When it occurs: When Claude determines it needs external data or computation—like fetching weather data, running a calculation, or querying a database. How to handle: Extract the tool call details, execute the tool in your environment, then send the result back as a tool_result block.
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": {
                "city": {"type": "string"}
            },
            "required": ["city"]
        }
    }],
    messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)

if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": tool_name = block.name tool_input = block.input tool_use_id = block.id # Execute your tool logic here result = execute_tool(tool_name, tool_input) # Then send result back in next request

3. max_tokens

Meaning: Claude stopped because it reached the max_tokens limit you set in your request. The response is incomplete. When it occurs: When the model needs more tokens to finish its thought, or when your max_tokens is set too low for the task. How to handle: Send the response back to Claude in a new message to let it continue. This is especially important for long-form generation or complex reasoning.
if response.stop_reason == "max_tokens":
    # Append Claude's response and ask it to continue
    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=2048,  # Consider increasing the limit
        messages=messages
    )

4. stop_sequence

Meaning: 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. When it occurs: When you've set stop_sequences in your API request and Claude generates one of those sequences. How to handle: The response is complete per your custom criteria. Process it as needed. The stop_sequence field in the response will contain the actual sequence that triggered the stop.
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}") print(response.content[0].text)

Handling Empty Responses with end_turn

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

Why It Happens

  • Adding text after tool results: If you insert a text block immediately after a tool_result, Claude may interpret this as the user taking over and end its turn.
  • Sending Claude's completed response back unchanged: If Claude already decided it's done, sending the same response back won't change its mind.

How to Prevent Empty Responses

Incorrect approach:
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 here
    ]}
]
Correct approach:
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 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"})
        response = client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    
    return response

Building a Complete Conversation Loop

For production applications, you'll want to handle all stop reasons in a single loop:

def converse_with_claude(client, messages, tools=None):
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            messages=messages,
            tools=tools
        )
        
        if response.stop_reason == "end_turn":
            # Claude is done—return the final response
            return response
            
        elif response.stop_reason == "tool_use":
            # Execute tools and append results
            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":
            # Let Claude continue
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
            
        elif response.stop_reason == "stop_sequence":
            # Custom stop reached—return as-is
            return response

Best Practices

  • Always check stop_reason before processing content. Don't assume end_turn means the response is complete—it could be empty.
  • Never add text after tool_result blocks. Send tool results directly without commentary.
  • Use continuation prompts for max_tokens. Don't just increase max_tokens blindly—let Claude continue naturally.
  • Log stop_reason for debugging. It helps identify patterns in your application's behavior.
  • Handle empty responses gracefully. Implement the recovery pattern shown above to avoid silent failures.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), tool_use (tool call needed), max_tokens (output truncated), and stop_sequence (custom delimiter reached).
  • Empty responses with end_turn are common in tool-use scenarios—prevent them by not adding text after tool_result blocks.
  • Always check stop_reason in your application logic to determine the next action, whether that's presenting output, executing a tool, or continuing the conversation.
  • Build a conversation loop that handles all stop reasons to create robust, production-ready Claude integrations.
  • Use continuation prompts ("Please continue") to recover from max_tokens stops or empty responses.