BeClaude
GuideBeginnerAgents2026-05-22

Mastering Claude's Stop Reasons: A Practical Guide to Handling API Responses

Learn how to interpret and handle Claude's stop_reason field in the Messages API, including end_turn, max_tokens, tool_use, and stop_sequence with practical code examples.

Quick Answer

Claude's stop_reason field tells you why the model stopped generating—end_turn for natural completion, max_tokens for truncation, tool_use for tool calls, and stop_sequence for custom stops. This guide shows how to handle each case in your application.

Messages APIstop_reasonerror handlingtool usebest practices

Introduction

When you call the Claude Messages API, every successful response includes a stop_reason field. This field is your key to understanding why Claude stopped generating—whether it finished naturally, hit a token limit, needs to call a tool, or encountered a custom stop sequence. Misinterpreting these values leads to broken workflows, truncated responses, or infinite loops.

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

The stop_reason Field

The stop_reason field is part of every successful Messages API response. Unlike errors (which indicate failures), 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
  }
}

Stop Reason Values

Claude can return four distinct stop_reason values. Each requires a different handling strategy.

end_turn — Natural Completion

What it means: Claude finished its response naturally and has nothing more to say. This is the most common stop reason and usually indicates a successful, complete response. How to handle it: Simply process the response content. 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": # 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_result blocks
  • Sending Claude's completed response back without adding anything new
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, add a continuation prompt in a new user message:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        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 with your response."
        })
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response

max_tokens — Token Limit Reached

What it means: Claude stopped because it reached the max_tokens limit you set. The response is likely truncated—Claude had more to say but ran out of space. How to handle it: You need to continue the conversation by sending Claude's response back along with a continuation prompt.
def handle_max_tokens(client, messages, response):
    if response.stop_reason == "max_tokens":
        # Append Claude's response to the message history
        messages.append({"role": "assistant", "content": response.content})
        # Add a continuation prompt
        messages.append({"role": "user", "content": "Please continue."})
        
        # Make a new request with higher max_tokens if needed
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=2048,  # Increase limit
            messages=messages
        )
    return response
Pro tip: If you're streaming responses, you can detect max_tokens early and automatically increase the limit for the next chunk.

tool_use — Tool Call Requested

What it means: Claude wants to call a tool (function). The response contains one or more tool_use content blocks. You must execute the tool and return results before Claude can continue. How to handle it:
def handle_tool_use(client, messages, response):
    if response.stop_reason == "tool_use":
        # Append Claude's response to history
        messages.append({"role": "assistant", "content": response.content})
        
        # Process each tool use block
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                # Execute the tool (your implementation)
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                })
        
        # Send results back to Claude
        messages.append({"role": "user", "content": tool_results})
        
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response
Important: Always append Claude's full response (including tool use blocks) to the message history before adding tool results. This maintains the conversation context.

stop_sequence — Custom Stop Sequence

What it means: Claude encountered one of your custom stop_sequences (defined in the API request) and stopped. This is useful for structured outputs or early termination. How to handle it:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:", "\n\nAssistant:"],
    messages=[{"role": "user", "content": "List 3 colors."}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # Process the response up to the stop sequence print(response.content[0].text)

Building a Robust Handler

Combine all four cases into a single handler for production applications:

def handle_claude_response(client, messages, response, max_iterations=10):
    iteration = 0
    while iteration < max_iterations:
        iteration += 1
        
        if response.stop_reason == "end_turn":
            # Final response
            return response.content[0].text if response.content else ""
        
        elif response.stop_reason == "max_tokens":
            # Continue generation
            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,
                messages=messages
            )
        
        elif response.stop_reason == "tool_use":
            # Execute tools
            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})
            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=messages
            )
        
        elif response.stop_reason == "stop_sequence":
            # Custom stop reached
            return response.content[0].text if response.content else ""
    
    raise Exception("Max iterations reached without end_turn")

Common Pitfalls and Solutions

PitfallSolution
Empty response with end_turnAdd a continuation prompt in a new user message
Infinite tool call loopsSet a maximum iteration limit (e.g., 10)
Truncated responsesIncrease max_tokens or implement continuation logic
Missing tool resultsAlways append Claude's full response before adding tool results

Key Takeaways

  • end_turn means Claude finished naturally—process the response as-is, but watch for empty responses after tool calls.
  • max_tokens means the response was truncated—send Claude's response back with a continuation prompt and consider increasing max_tokens.
  • tool_use means Claude needs to call a tool—execute the tool and return results in a new user message with tool_result blocks.
  • stop_sequence means a custom stop sequence was hit—handle it according to your application logic.
  • Always implement a maximum iteration limit when handling tool_use or max_tokens to prevent infinite loops.