BeClaude
GuideBeginnerAgents2026-05-22

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

Learn how to handle Claude API stop_reason values (end_turn, tool_use, max_tokens) to build reliable applications. Includes code examples and troubleshooting tips.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, tool_use, max_tokens, and stop_sequence—and shows you how to handle each one in your code to prevent empty responses, continue tool loops, and manage token limits.

stop_reasonClaude APItool_useerror handlingMessages API

Introduction

Every time you call the Claude Messages API, the response includes a stop_reason field. This tiny piece of data tells you why Claude stopped generating—whether it finished naturally, requested a tool call, hit a token limit, or encountered a stop sequence. Ignoring stop_reason is like driving without looking at your dashboard: you might get where you're going, but you'll miss critical signals along the way.

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

Understanding the stop_reason Field

The stop_reason field appears in every successful Messages API response. It's not an error—it's a signal about completion. Here's a typical response:

{
  "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
  }
}

There are four possible values for stop_reason:

ValueMeaning
end_turnClaude finished its response naturally
tool_useClaude wants to call a tool (function)
max_tokensClaude hit the max_tokens limit you set
stop_sequenceClaude encountered a custom stop sequence
Let's dive into each one.

1. end_turn: The Natural Finish

end_turn is the most common stop reason. It means Claude believes it has completed its response and is ready for the next user input. In most cases, you can simply display 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); }

The Empty Response Problem

Sometimes Claude returns an empty response (2–3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use scenarios when:

  • You add text blocks immediately after tool_result blocks
  • You send Claude's completed response back without adding anything new
Why it happens: Claude learns patterns from your conversation history. If you always insert text after tool results, Claude may end its turn prematurely, expecting you to continue. How to fix it:
# 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 continuation prompt in a new user message:

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
        messages.append({
            "role": "user",
            "content": "Please continue with your response."
        })
        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 API call), it returns stop_reason: "tool_use" along with one or more tool_use content blocks. Your application must:

  • Extract the tool call details
  • Execute the tool
  • Return the result as a tool_result block
  • Continue the conversation

Python Tool Loop

def run_tool_loop(client, messages):
    while True:
        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=messages
        )
        
        if response.stop_reason == "end_turn":
            return response.content[0].text
        
        if response.stop_reason == "tool_use":
            for block in response.content:
                if block.type == "tool_use":
                    # Execute the tool
                    result = execute_tool(block.name, block.input)
                    # Append assistant's response and tool result
                    messages.append({"role": "assistant", "content": response.content})
                    messages.append({
                        "role": "user",
                        "content": [{
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": str(result)
                        }]
                    })

TypeScript Tool Loop

async function runToolLoop(client: Anthropic, messages: any[]) {
  while (true) {
    const response = await client.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      tools: [/ your tool definitions /],
      messages
    });

if (response.stop_reason === 'end_turn') { return response.content[0].text; }

if (response.stop_reason === 'tool_use') { for (const block of response.content) { if (block.type === 'tool_use') { const result = await executeTool(block.name, block.input); messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: block.id, content: String(result) }] }); } } } } }

3. max_tokens: Hit the Token Limit

When Claude's response is cut off because it reached the max_tokens limit, you get stop_reason: "max_tokens". This means the response is incomplete. To continue, send a follow-up message asking Claude to finish.

def handle_max_tokens(client, messages, response):
    if response.stop_reason == "max_tokens":
        # Append the partial response
        messages.append({"role": "assistant", "content": response.content})
        # Ask Claude to continue
        messages.append({
            "role": "user",
            "content": "Please continue from where you left off."
        })
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response
Pro tip: Increase max_tokens if you frequently hit this limit, or implement automatic continuation in a loop.

4. stop_sequence: Custom Stop Sequences

If you defined custom stop_sequences in your API request, Claude will stop when it encounters one. The stop_sequence field will contain the matched sequence.

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:"],
    messages=[{"role": "user", "content": "Tell me a story"}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # The response is complete up to the stop sequence

Best Practices Summary

  • Always check stop_reason before processing the response content
  • For tool_use: Implement a loop that executes tools and returns results
  • For max_tokens: Continue the conversation with a prompt to finish
  • For end_turn: Display the response, but watch for empty responses in tool contexts
  • Avoid adding text after tool_result blocks to prevent empty responses

Key Takeaways

  • stop_reason tells you why Claude stopped, not that something went wrong—it's a completion signal, not an error
  • end_turn means Claude finished naturally; handle empty responses by sending a continuation prompt in a new user message
  • tool_use requires a loop: extract the tool call, execute it, return the result, and continue the conversation
  • max_tokens means the response is incomplete; send a follow-up prompt to let Claude finish
  • stop_sequence is triggered by custom sequences you define; the matched sequence is returned in the response