BeClaude
GuideBeginnerAgents2026-05-17

Mastering Claude's Stop Reasons: A Practical Guide to Handling API Responses

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

Quick Answer

This guide explains Claude's stop_reason field—why the model stops generating—and how to handle each value (end_turn, max_tokens, tool_use, stop_sequence) in your API applications. You'll learn to detect empty responses, manage tool calls, and build robust conversational loops.

Messages APIstop_reasontool useerror handlingAPI best practices

Mastering Claude's Stop Reasons: A Practical Guide to Handling API Responses

Every time you call Claude's Messages API, the response includes a stop_reason field. This small but critical piece of data tells you why the model stopped generating—whether it finished naturally, hit a token limit, or wants to use a tool. Understanding and correctly handling these stop reasons is essential for building reliable, production-ready applications.

In this guide, we'll explore each stop reason value, show you how to handle them in code, and share best practices to avoid common pitfalls like empty responses.

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. It's your signal for what to do next.

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 stop_reason can be one of four values: end_turn, max_tokens, tool_use, or stop_sequence. Let's dive into each.

The Four Stop Reasons

1. end_turn – Natural Completion

This is the most common stop reason. It means Claude finished its response naturally—it decided the conversation turn was complete and voluntarily stopped.

When to expect it: After Claude answers a question, provides information, or completes a task without needing tools. How to handle it: Simply process the response content and wait for the next user input.
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)

#### The Empty Response Gotcha

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's turn is complete, particularly after tool results.

Common causes:
  • Adding text blocks immediately after tool results in the message array
  • Sending Claude's completed response back without adding anything new
How to prevent it:
# 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, no additional text ]

If you still get empty responses, use a continuation prompt:

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

2. max_tokens – Token Limit Reached

This stop reason means Claude reached the max_tokens limit you set in your API request before it could finish its thought.

When to expect it: For long responses, complex reasoning, or when you set a low max_tokens value. How to handle it: You need to continue the conversation by sending Claude's response back along with a prompt to continue.
def handle_max_tokens(client, messages, response):
    if response.stop_reason == "max_tokens":
        # Append Claude's response to the message history
        messages.append({"role": "assistant", "content": response.content})
        # Ask Claude to continue
        messages.append({"role": "user", "content": "Please continue"})
        
        # Make a new request
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response
Best practice: Set max_tokens generously (e.g., 4096 or higher) to minimize interruptions, especially for complex tasks.

3. tool_use – Claude Wants to Use a Tool

This stop reason indicates Claude decided to call one or more tools. The response content array will contain tool_use content blocks.

When to expect it: When you've defined tools and Claude determines it needs to use them. How to handle it: Execute the tool calls, then send the results back to Claude.
def handle_tool_use(client, messages, response):
    if response.stop_reason == "tool_use":
        # Append Claude's response (with tool_use blocks) to messages
        messages.append({"role": "assistant", "content": response.content})
        
        # Process each tool call
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                # Execute the tool (pseudocode)
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                })
        
        # Send results back to Claude
        messages.append({"role": "user", "content": tool_results})
        
        # Get Claude's next response
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response
Important: After sending tool results, Claude may respond with another tool_use, max_tokens, or end_turn. Your code should loop until you get end_turn.

4. stop_sequence – Custom Stop Sequence Triggered

This stop reason means Claude encountered a custom stop sequence you defined in your API request.

When to expect it: When you set stop_sequences in your request (e.g., ["\n\nHuman:"]). How to handle it: The response is complete up to the stop sequence. Process it as a finished response.
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:"],
    messages=[{"role": "user", "content": "Tell me a short story"}]
)

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

Building a Robust Response Handler

Here's a complete loop that handles all stop reasons gracefully:

from anthropic import Anthropic

client = Anthropic()

def process_conversation(messages, max_iterations=10): """ Process a conversation, handling all stop reasons. Returns the final response when Claude finishes naturally. """ for i in range(max_iterations): response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=4096, messages=messages ) if response.stop_reason == "end_turn": # Check for empty response if not response.content: messages.append({"role": "user", "content": "Please continue"}) continue return response elif response.stop_reason == "max_tokens": messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": "Please continue"}) elif response.stop_reason == "tool_use": messages.append({"role": "assistant", "content": response.content}) tool_results = [] for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(result) }) messages.append({"role": "user", "content": tool_results}) elif response.stop_reason == "stop_sequence": return response raise Exception("Max iterations reached without end_turn")

Common Pitfalls and Solutions

PitfallSolution
Empty response with end_turnUse a continuation prompt or fix message structure
Infinite loop with tool_useSet a maximum iteration limit
Truncated responses with max_tokensIncrease max_tokens or implement continuation logic
Ignoring stop_sequenceCheck for it and handle appropriately

Key Takeaways

  • stop_reason tells you why Claude stopped – it's not an error, but a signal for your next action
  • end_turn means Claude finished naturally; watch for empty responses and use continuation prompts if needed
  • max_tokens requires you to continue the conversation by sending Claude's response back with a "Please continue" prompt
  • tool_use means you need to execute the tool and send results back; loop until you get end_turn
  • Always check stop_reason in your API handler to build robust, production-ready applications