BeClaude
Guide2026-05-01

Mastering Claude's Stop Reasons: Build Smarter, More Reliable API Applications

Learn how to interpret and handle Claude's stop_reason field—end_turn, tool_use, max_tokens, and stop_sequence—with practical code examples and best practices for robust API apps.

Quick Answer

This guide explains Claude's four stop reasons—end_turn, tool_use, max_tokens, and stop_sequence—and shows you how to handle each one in your code to build reliable, production-ready applications.

Claude APIstop_reasonerror handlingtool useAPI best practices

Mastering Claude's Stop Reasons: Build Smarter, More Reliable API Applications

When you call the Claude API, every successful response includes a stop_reason field. This small 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 custom stop sequence. Understanding and handling these reasons is essential for building robust, production-ready applications.

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

What Is stop_reason?

The stop_reason field appears in every successful response from the Messages API. It is not an error—it’s a signal that Claude completed its generation and is telling you why it stopped.

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 Reasons

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

1. end_turn — Natural Completion

This is the most common stop reason. Claude finished its response naturally and has nothing more to say. You can safely present the response to the user.

Python example:
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)

TypeScript example:
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }] });

if (response.stop_reason === 'end_turn') { console.log(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 in tool-use workflows when:

  • You add text blocks immediately after tool_result blocks
  • You send Claude’s completed response back without adding anything new
How to prevent empty responses:
# INCORRECT: 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: 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 ]} ]

If you still get empty responses, add a fallback handler:

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:
        # Retry with a prompt that encourages output
        messages.append({"role": "user", "content": "Please continue."})
        return client.messages.create(
            model="claude-opus-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response

2. tool_use — Claude Wants to Call a Tool

When Claude decides it needs to use a tool (e.g., a calculator, database query, or web search), it stops with stop_reason: "tool_use". Your application must:

  • Read the tool call details from the response content
  • Execute the tool (e.g., run the calculation, query the database)
  • Send the result back as a tool_result block
Python example:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "calculator",
        "description": "Perform arithmetic operations",
        "input_schema": {
            "type": "object",
            "properties": {
                "operation": {"type": "string"},
                "a": {"type": "number"},
                "b": {"type": "number"}
            },
            "required": ["operation", "a", "b"]
        }
    }],
    messages=[{"role": "user", "content": "What is 1234 + 5678?"}]
)

if response.stop_reason == "tool_use": 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) messages.append({"role": "user", "content": [ {"type": "tool_result", "tool_use_id": block.id, "content": str(result)} ]}) # Continue the conversation final_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages )

3. max_tokens — Token Limit Reached

Claude stopped because it hit the max_tokens limit you set. The response may be cut off mid-sentence. You should:

  • Inform the user that the response was truncated
  • Optionally, continue the conversation by sending a follow-up message
Python example:
if response.stop_reason == "max_tokens":
    partial_text = response.content[0].text
    print(f"Response was truncated. Here's what we got:\n{partial_text}")
    # Continue by asking Claude to finish
    messages.append({"role": "assistant", "content": partial_text})
    messages.append({"role": "user", "content": "Please continue from where you left off."})
    continued_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

4. stop_sequence — Custom Stop Sequence Triggered

If you defined one or more stop_sequences in your API request, Claude stops when it encounters one. This is useful for:

  • Extracting structured data (e.g., stop at </output>)
  • Controlling conversation flow (e.g., stop at \n\nHuman:)
Example:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:"],
    messages=[{"role": "user", "content": "Tell me a story.\n\nHuman: Stop here."}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at custom sequence. Text: {response.content[0].text}") print(f"Triggered by: {response.stop_sequence}")

Building a Robust Handler

For production applications, you should handle all four stop reasons in a single loop. Here’s a pattern that works for tool-using agents:

def handle_conversation(client, messages, tools=None):
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages,
            tools=tools
        )
        
        if response.stop_reason == "end_turn":
            # Natural completion — return the response
            return response
        
        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({"role": "user", "content": [
                        {"type": "tool_result", "tool_use_id": block.id, "content": str(result)}
                    ]})
        
        elif response.stop_reason == "max_tokens":
            # Ask Claude to continue
            messages.append({"role": "assistant", "content": response.content[0].text})
            messages.append({"role": "user", "content": "Please continue."})
        
        elif response.stop_reason == "stop_sequence":
            # Custom stop — return as-is or handle specially
            return response

Common Pitfalls and Best Practices

PitfallSolution
Ignoring tool_useAlways check for tool calls and execute them before continuing
Not handling max_tokensDetect truncated responses and prompt Claude to continue
Empty end_turn responsesAvoid adding text after tool_result blocks; implement retry logic
Forgetting stop_sequenceIf you use custom stop sequences, handle them explicitly

Key Takeaways

  • end_turn means Claude finished naturally—present the response to the user, but watch for empty responses in tool workflows.
  • tool_use means Claude wants to call a tool—you must execute it and send the result back.
  • max_tokens means the response was cut off—prompt Claude to continue or inform the user.
  • stop_sequence means a custom stop sequence was triggered—handle it according to your application logic.
  • Always check stop_reason in every response and build a handler that covers all four cases for a robust, production-ready application.