BeClaude
GuideBeginnerAgents2026-05-16

Mastering Claude API Stop Reasons: A Practical Guide to Handling end_turn, tool_use, and max_tokens

Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes code examples, empty response fixes, and best practices.

Quick Answer

This guide explains Claude's stop_reason field — end_turn, tool_use, max_tokens, and stop_sequence — and shows how to handle each in your application. You'll learn to detect empty responses, continue tool-using conversations, and avoid common pitfalls.

stop_reasonMessages APItool useerror handlingClaude API

Introduction

When you send a request to the Claude API, the response includes a stop_reason field. This field tells you why Claude stopped generating — whether it finished naturally, wants to use a tool, hit a token limit, or encountered a stop sequence. Understanding these values is essential for building reliable, production-ready applications.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle them in Python and TypeScript
  • How to prevent and fix empty responses
  • Best practices for tool-using conversations
Let's dive in.

The stop_reason Field

The stop_reason is part of every successful Messages API response. Unlike errors (which indicate a failure), 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 possible stop_reason values:

1. end_turn

The most common stop reason. Claude finished its response naturally and handed control back to the user. This is what you'll see for simple Q&A or when Claude has nothing more to say.

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. tool_use

Claude wants to call a tool. The response will contain one or more tool_use content blocks. You must execute the tool and return the result in a new user message with a tool_result block.

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

3. max_tokens

Claude reached the max_tokens limit you set. The response is truncated. This often happens with long outputs or complex reasoning.

How to handle it:
if response.stop_reason == "max_tokens":
    # Append Claude's partial response
    messages.append({"role": "assistant", "content": response.content})
    # Ask Claude to continue
    messages.append({"role": "user", "content": "Please continue."})
    
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,  # Consider increasing
        messages=messages
    )

4. stop_sequence

Claude encountered a custom stop sequence you defined in your API request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter.

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": "Tell me a story."}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # The content may be partial; handle accordingly print(response.content[0].text)

Handling 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, especially after tool results.

Common Causes

  • Adding text blocks immediately after tool results — Claude learns to expect the user to always insert text after tool results, so it ends its turn to follow the pattern.
  • Sending Claude's completed response back without adding anything — Claude already decided it's done, so it will remain done.

How to Prevent Empty Responses

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

Recovery from Empty Responses

If you still get empty responses after fixing the above, use a continuation prompt:

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

Building a Robust Response Handler

Here's a complete handler that deals with all stop reasons:

from anthropic import Anthropic

client = Anthropic()

def handle_response(response, messages): """Process Claude's response and handle all stop reasons.""" if response.stop_reason == "end_turn": if not response.content: # Empty response recovery messages.append({"role": "user", "content": "Please continue"}) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages ) # Normal end_turn - conversation is complete 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) }] }) # Continue the conversation new_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages ) return handle_response(new_response, messages) elif response.stop_reason == "max_tokens": # Append partial response and ask to continue messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": "Please continue."}) new_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2048, messages=messages ) return handle_response(new_response, messages) elif response.stop_reason == "stop_sequence": # Custom stop sequence encountered return response else: raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Best Practices

  • Always check stop_reason — Don't assume Claude finished naturally. Handle tool_use and max_tokens explicitly.
  • Never add text after tool_result — Keep tool results clean. Let Claude decide what to say.
  • Use continuation prompts for max_tokens — Append Claude's partial output and ask it to continue.
  • Set appropriate max_tokens — For complex tasks, start with 2048 or higher.
  • Log stop_reason for debugging — It helps identify patterns in your application's behavior.

Key Takeaways

  • Four stop reasons: end_turn (natural finish), tool_use (wants to call a tool), max_tokens (output truncated), and stop_sequence (custom delimiter hit).
  • Handle tool_use by executing the tool and returning results in a tool_result block — never add extra text after it.
  • Recover from max_tokens by appending Claude's partial response and asking it to continue with a higher limit.
  • Prevent empty end_turn responses by keeping tool results clean and using continuation prompts when needed.
  • Build a recursive handler that processes all stop reasons until you get a complete end_turn response.