BeClaude
Guide2026-05-05

Mastering Claude API Stop Reasons: A Practical Guide to Handling Response Endings

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

Quick Answer

This guide explains Claude's stop_reason field (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each in your application. You'll learn to detect empty responses, continue conversations properly, and build robust multi-turn interactions.

Claude APIstop_reasonerror handlingtool usebest practices

Introduction

When you send a request to the Claude API, the response includes a stop_reason field that tells you why the model stopped generating. This isn't an error—it's a signal. Understanding these signals is essential for building reliable applications that handle tool calls, truncated responses, and natural conversation endings correctly.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle end_turn, tool_use, max_tokens, and stop_sequence
  • How to prevent and recover from empty responses
  • Best practices for multi-turn conversations with tools

The stop_reason Field

Every successful response from the Messages API includes a stop_reason field. 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

end_turn

Most common. Claude finished its response naturally—it decided the assistant's turn was complete. This is the default for simple Q&A.
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)

tool_use

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

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=messages,
    tools=[{
        "name": "get_weather",
        "description": "Get current weather",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string"}
            }
        }
    }]
)

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

max_tokens

Claude hit the max_tokens limit you set. The response is truncated. To continue, append a new user message with a continuation prompt like "Please continue."

if response.stop_reason == "max_tokens":
    # The response was cut off
    partial_text = response.content[0].text
    
    # 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
    )

stop_sequence

Claude encountered a custom stop sequence you defined in your request. This is useful for structured outputs where you want to stop generation at a specific marker.

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "List three colors"}],
    stop_sequences=["\n"]  # Stop at first newline
)

if response.stop_reason == "stop_sequence": print("Stopped at custom sequence:", response.stop_sequence)

Handling Empty Responses with end_turn

Sometimes Claude returns an empty response (2–3 tokens, no content) with stop_reason: "end_turn". This typically happens after tool results when Claude decides the assistant turn is complete.

Common Causes

  • Adding text after tool results – Claude learns to expect the user to insert text after tool results, so it ends its turn to follow the pattern.
  • Sending Claude's completed response back unchanged – Claude already decided it's done, so it stays done.

How to Prevent Empty Responses

Incorrect: Adding text immediately after tool_result
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: Send tool results directly without additional text
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
        ]
    }
]

Recovering from Empty Responses

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

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

Building a Robust Response Handler

Here's a complete handler that processes all stop reasons correctly:

def handle_claude_response(client, messages, tools=None):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages,
        tools=tools
    )
    
    if response.stop_reason == "end_turn":
        if not response.content:
            # Empty response – prompt to continue
            messages.append({"role": "user", "content": "Please continue"})
            return handle_claude_response(client, messages, tools)
        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)
                    }]
                })
        # Continue the conversation
        return handle_claude_response(client, messages, tools)
    
    elif response.stop_reason == "max_tokens":
        messages.append({"role": "user", "content": "Please continue"})
        return handle_claude_response(client, messages, tools)
    
    elif response.stop_reason == "stop_sequence":
        return response.content[0].text

Best Practices

  • Always check stop_reason – Don't assume the response is complete. Handle each case explicitly.
  • Never add text after tool_result – Send only the tool result in the user message to avoid empty responses.
  • Use continuation prompts for truncation – When max_tokens is hit, append a new user message with "Please continue" rather than retrying the same prompt.
  • Log stop reasons – In production, log stop_reason to debug unexpected behavior and monitor usage patterns.
  • Set appropriate max_tokens – For long-form content, set max_tokens high enough to avoid frequent truncation.

Key Takeaways

  • end_turn means Claude finished naturally; tool_use means it wants to call a tool; max_tokens means the response was truncated; stop_sequence means a custom stop marker was hit.
  • Empty responses with end_turn are usually caused by adding text after tool_result blocks—send only the tool result.
  • Recover from empty responses by appending a new user message with "Please continue"—don't retry the same messages.
  • Build a recursive handler that processes tool calls and truncation automatically for robust multi-turn conversations.
  • Log and monitor stop reasons in production to catch issues early and optimize your application's behavior.