BeClaude
GuideBeginnerAgents2026-05-15

Mastering Claude's Stop Reasons: Build Robust API Applications

Learn how to interpret and handle Claude's stop_reason field in the Messages API. This guide covers end_turn, max_tokens, tool_use, and error handling with practical code examples.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and shows how to handle each in your API code to build reliable, production-ready applications.

stop_reasonMessages APIerror handlingtool usebest practices

Mastering Claude's Stop Reasons: Build Robust API Applications

When you send a request to Claude via the Messages API, the response includes a stop_reason field that tells you why the model stopped generating. Understanding these values is essential for building applications that handle different scenarios gracefully—whether Claude finished naturally, hit a token limit, or paused to use a tool.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle them in Python and TypeScript
  • Common pitfalls (like empty responses) and how to avoid them
  • Best practices for production-ready code

What Is stop_reason?

The stop_reason field is part of every successful Messages API response. Unlike error codes (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
  }
}

The Four Stop Reasons

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

end_turn

Meaning: Claude finished its response naturally. The model determined it had fully answered the user's request and voluntarily ended its turn.

This is the most common stop reason and usually indicates a successful, complete response.

Handling in Python:
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)

Handling in TypeScript:
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); }

max_tokens

Meaning: Claude stopped because it reached the max_tokens limit you set in your request. The response may be truncated—Claude had more to say but was cut off.

This is critical to handle because the user might receive an incomplete answer.

How to handle it:
if response.stop_reason == "max_tokens":
    # The response is truncated. Continue the conversation to get more.
    messages.append({
        "role": "assistant",
        "content": response.content
    })
    messages.append({
        "role": "user",
        "content": "Please continue."
    })
    
    continuation = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )
Best practice: Consider increasing max_tokens for complex tasks, or implement automatic continuation logic as shown above.

tool_use

Meaning: Claude stopped because it wants to use a tool (function call). The response will contain one or more tool_use content blocks.

This is the expected behavior when you've provided tools to Claude. Your application must:

  • Extract the tool use details from the response
  • Execute the tool (e.g., call an API, query a database)
  • Return the result as a tool_result block in a new user message
Example flow:
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_id = block.id
            
            # Execute the tool
            result = execute_tool(tool_name, tool_input)
            
            # Add tool result to messages
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool_id,
                    "content": str(result)
                }]
            })
    
    # Continue the conversation
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

stop_sequence

Meaning: Claude stopped because it encountered a custom stop sequence you defined in your API request. This is useful for controlling output format—for example, stopping after a closing XML tag or a specific delimiter. Example:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["</answer>", "\n\n"],
    messages=[{"role": "user", "content": "Give me a short answer.</answer>"}]
)

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 pitfall: Claude sometimes returns an empty response (2–3 tokens with 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 learn to expect that pattern and end its turn prematurely.
  • 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 It

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 after tool_result
    ]}
]
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 tool_result
    ]}
]

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
        messages.append({"role": "user", "content": "Please continue"})
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    
    return response

Building a Robust Handler

For production applications, you need a handler that covers all scenarios:

def handle_claude_response(client, messages, max_iterations=10):
    """
    Handle Claude's response, including tool use and continuation.
    """
    iteration = 0
    
    while iteration < max_iterations:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            if not response.content:
                # Empty response - prompt to continue
                messages.append({"role": "user", "content": "Please continue"})
                iteration += 1
                continue
            return response.content[0].text
        
        elif response.stop_reason == "tool_use":
            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":
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
        
        elif response.stop_reason == "stop_sequence":
            return response.content[0].text
        
        iteration += 1
    
    raise Exception("Max iterations reached without completion")

Key Takeaways

  • stop_reason tells you why Claude stopped—not that something went wrong. Use it to decide your next action.
  • end_turn means a natural completion; max_tokens means truncation; tool_use means Claude wants to call a function; stop_sequence means a custom delimiter was hit.
  • Empty responses with end_turn are common in tool-use flows. Prevent them by sending only tool_result blocks (no extra text) and use a "Please continue" prompt if needed.
  • Always handle max_tokens by continuing the conversation—otherwise users get incomplete answers.
  • Build a loop for tool-use scenarios that automatically executes tools and returns results until Claude produces a final answer.