BeClaude
Guide2026-04-22

Mastering Claude's Stop Reasons: Build Smarter API Applications

Learn how to interpret and handle Claude's stop_reason field in the Messages API. Includes code examples for end_turn, max_tokens, tool_use, and error handling strategies.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and how to handle each in your API application to build robust, production-ready integrations.

Claude APIstop_reasonerror handlingtool useMessages API

When you call the Claude Messages API, every successful response includes a stop_reason field. This small but powerful piece of data tells you why Claude stopped generating—whether it finished naturally, hit a token limit, requested a tool call, or matched a custom stop sequence. Understanding these reasons is essential for building applications that respond intelligently to different scenarios.

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 API response. It is not an error—it's a signal that Claude completed its response 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 return one of four stop_reason values:

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

end_turn: Natural Completion

This is the most common stop reason. Claude decided it had fully answered the user's request and stopped on its own.

Python example:
from anthropic import Anthropic

client = Anthropic() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "What is the capital of France?"}] )

if response.stop_reason == "end_turn": print("Claude finished naturally:") print(response.content[0].text)

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

const client = new Anthropic();

async function main() { const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'What is the capital of France?' }] });

if (response.stop_reason === 'end_turn') { console.log('Claude finished naturally:'); console.log(response.content[0].text); } }

#### 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 in tool-use workflows when Claude decides the assistant's turn is complete.

Common causes:
  • Adding text blocks immediately after tool_result blocks
  • Sending 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"} ] # No extra text } ]

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-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 with your response." }) # Retry response = client.messages.create( model="claude-opus-4-7", max_tokens=1024, messages=messages ) return response

max_tokens: Token Limit Reached

Claude stopped because it hit the max_tokens limit you set. This is common for long responses.

How to handle it:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=200,  # Low limit for demonstration
    messages=[{"role": "user", "content": "Write a detailed essay on AI safety."}]
)

if response.stop_reason == "max_tokens": print("Response was truncated. Consider increasing max_tokens.") # You can continue the conversation by sending Claude's response back messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": "Please continue."}) response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, messages=messages )

Best practice: Set max_tokens generously for open-ended tasks, or implement a loop that continues the conversation when max_tokens is hit.

tool_use: Claude Wants to Call a Tool

When you provide tools (functions), Claude may decide to call one. The response will contain a tool_use content block.

Handling tool_use:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string"}
            },
            "required": ["location"]
        }
    }],
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)

if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": tool_name = block.name tool_input = block.input print(f"Claude wants to call {tool_name} with {tool_input}") # Execute the tool and send result back result = execute_tool(tool_name, tool_input) messages.append({"role": "assistant", "content": response.content}) 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 )

stop_sequence: Custom Stop Sequence Matched

If you set a stop_sequences parameter in your API call, Claude will stop when it encounters that sequence.

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

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

Building a Robust Handler

Combine all stop reasons into a single handler:

def handle_response(response, messages, client):
    if response.stop_reason == "end_turn":
        if not response.content:
            # Handle empty response
            messages.append({"role": "user", "content": "Please continue."})
            return client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=messages
            )
        return response  # Complete

elif response.stop_reason == "max_tokens": # Continue the conversation messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": "Please continue."}) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages )

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

elif response.stop_reason == "stop_sequence": # Handle custom stop print(f"Custom stop at: {response.stop_sequence}") return response

else: raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Key Takeaways

  • stop_reason is always present in successful API responses and tells you why Claude stopped generating.
  • end_turn means natural completion—but watch for empty responses in tool workflows.
  • max_tokens means your token limit was hit; implement a continuation loop for long responses.
  • tool_use means Claude wants to call a function; execute the tool and send results back.
  • stop_sequence means a custom stop pattern was matched; handle it based on your application logic.
  • Always check stop_reason in your code to build robust, production-ready Claude integrations.