BeClaude
GuideBeginnerAgents2026-05-15

Mastering Claude API Stop Reasons: Build Robust Applications with end_turn, max_tokens & Tool Use

Learn how to interpret and handle Claude API stop_reason values like end_turn, max_tokens, and tool_use. Practical code examples for building reliable AI applications.

Quick Answer

This guide explains the four stop_reason values in Claude's Messages API (end_turn, max_tokens, tool_use, stop_sequence), how to handle empty responses, and best practices for building robust applications that respond appropriately to each stop condition.

stop_reasonMessages APItool useerror handlingClaude API

Mastering Claude API Stop Reasons: Build Robust Applications

When building applications with Claude's Messages API, understanding why the model stopped generating its response is just as important as the response itself. The stop_reason field tells you exactly that—and handling it correctly can mean the difference between a smooth user experience and a broken workflow.

In this guide, you'll learn what each stop reason means, how to handle them in code, and how to avoid common pitfalls like empty responses.

What Is the stop_reason Field?

Every successful response from the Messages API includes a stop_reason field. Unlike HTTP errors (which indicate a failed request), 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

Claude can stop generating for four distinct reasons. Let's explore each one.

1. end_turn – Natural Completion

Meaning: Claude finished its response naturally. The model decided it had said everything needed and ended its turn.

This is the most common stop reason and generally indicates success. However, there's a tricky edge case we'll cover below.

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. max_tokens – Token Limit Reached

Meaning: Claude stopped because it hit the max_tokens limit you set in your request. The response may be truncated—Claude had more to say but ran out of room. How to handle it:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=150,  # intentionally low
    messages=[{"role": "user", "content": "Write a long story about a robot."}]
)

if response.stop_reason == "max_tokens": # The response is incomplete. You can: # 1. Increase max_tokens and retry # 2. Ask Claude to continue print("Response was truncated. Asking Claude to continue...") # Add the current response to history and ask for continuation messages = [ {"role": "user", "content": "Write a long story about a robot."}, {"role": "assistant", "content": response.content[0].text}, {"role": "user", "content": "Please continue from where you left off."} ] continuation = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages )

3. tool_use – Tool Call Requested

Meaning: Claude decided it needs to use a tool (function call) to complete your request. The response content will contain one or more tool_use blocks. 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 tool calls from content for block in response.content: if block.type == "tool_use": tool_name = block.name tool_input = block.input tool_id = block.id print(f"Claude wants to call: {tool_name}") print(f"With input: {tool_input}") # Execute the tool and send result back result = execute_tool(tool_name, tool_input) # Continue the conversation with tool result messages = [ {"role": "user", "content": "What's the weather in Tokyo?"}, response, # assistant's response with tool_use { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": str(result) } ] } ] final_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[...], messages=messages )

4. stop_sequence – Custom Stop Sequence

Meaning: Claude encountered a custom stop sequence you defined in your request. This is rare and only happens when you explicitly set stop_sequences. 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 5 fruits. End with END."}]
)

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

The Empty Response Problem (and How to Fix It)

A common frustration: Claude returns an empty response with stop_reason: "end_turn". This typically happens in tool use workflows.

Why It Happens

Claude sometimes interprets that the assistant's turn is complete—especially after tool results. Two common causes:

  • Adding text blocks after tool results – Claude learns a pattern where users always insert text after tool results, so it ends its turn to follow that pattern.
  • Sending Claude's completed response back unchanged – Claude already decided it was done, so it stays done.

How to Prevent It

Incorrect pattern:
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 add text after tool_result
        ]
    }
]
Correct pattern:
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 tool_result
        ]
    }
]

Handling Empty Responses When They Still Occur

If you've fixed the pattern above but still get empty responses, use a continuation prompt:

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

Building a Robust Response Handler

Here's a complete handler that manages all stop reasons gracefully:

from anthropic import Anthropic

client = Anthropic()

def handle_claude_response(response, messages, tools=None): """Process Claude's response based on stop_reason.""" if response.stop_reason == "end_turn": if not response.content: # Empty response - ask Claude to continue messages.append({"role": "user", "content": "Please continue"}) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages, tools=tools ) else: # Normal completion - return the response return response elif response.stop_reason == "max_tokens": # Response truncated - ask for continuation messages.append(response) messages.append({"role": "user", "content": "Please continue from where you left off."}) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2048, # Increase limit messages=messages, tools=tools ) elif response.stop_reason == "tool_use": # Execute tools and continue for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) messages.append(response) messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "content": str(result) }] }) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages, tools=tools ) elif response.stop_reason == "stop_sequence": # Custom stop sequence reached - return as-is return response else: raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Best Practices Summary

  • Always check stop_reason – Don't assume end_turn means success. Check for max_tokens to detect truncation.
  • Handle tool_use explicitly – If your app uses tools, you must loop until you get end_turn or max_tokens.
  • Avoid text after tool_result – Keep tool results clean to prevent empty responses.
  • Use continuation prompts for truncation – When max_tokens is hit, ask Claude to continue rather than retrying the same prompt.
  • Log stop_reason for debugging – It's invaluable for understanding unexpected behavior.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), max_tokens (truncated), tool_use (tool call needed), and stop_sequence (custom stop).
  • Empty responses with end_turn are usually caused by adding text after tool_result blocks—remove the extra text to fix it.
  • For truncated responses (max_tokens), use a continuation prompt rather than retrying the original request.
  • Tool use requires a loop: Keep calling the API with tool results until Claude returns end_turn or max_tokens.
  • Always handle stop_reason in your code to build robust, production-ready applications that gracefully handle every scenario.