BeClaude
GuideBeginnerAPI2026-05-22

Mastering Claude API Stop Reasons: Build Smarter, More Reliable Applications

Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Practical code examples and troubleshooting tips included.

Quick Answer

This guide explains the stop_reason field in Claude's API responses, covering each value (end_turn, tool_use, max_tokens, stop_sequence) with code examples. You'll learn how to handle empty responses, continue conversations after tool calls, and build robust multi-turn interactions.

stop_reasonMessages APItool_useerror handlingClaude API

Introduction

When you send a request to the Claude API, the model generates a response and then stops. But why did it stop? That's where the stop_reason field comes in. This small but critical piece of data tells you exactly why Claude ended its response—whether it finished naturally, requested to use a tool, hit a token limit, or encountered a stop sequence.

Understanding stop_reason is essential for building reliable, production-ready applications. Without it, you might miss a tool call, cut off a response prematurely, or get stuck in infinite loops. In this guide, we'll break down each stop reason, show you how to handle them in code, and share best practices to avoid common pitfalls.

What Is the stop_reason Field?

The stop_reason field is part of every successful response from the Messages API. It's not an error—it's a signal that tells you the model finished generating for a specific reason. 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 possible values for stop_reason are:

  • end_turn – Claude finished its response naturally.
  • tool_use – Claude wants to call a tool (function).
  • max_tokens – The response was cut off because it reached the max_tokens limit.
  • stop_sequence – A custom stop sequence you provided was encountered.
Let's dive into each one.

end_turn: The Natural Stop

end_turn is the most common stop reason. It means Claude decided it had completed its response and voluntarily stopped. This is the ideal outcome for simple Q&A or single-turn interactions.

Handling end_turn in Python

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 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 this happens: Claude learns patterns from your message history. If you always insert text after tool results, Claude learns to expect that and ends its turn early. If you send back the exact same response, Claude sees it's already done and stays done.

#### How to Prevent Empty Responses

Incorrect approach: Adding text after tool results.
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 do this
    ]}
]
Correct approach: Send only the 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"}  # ✅ Just the result
    ]}
]

#### Handling Persistent Empty Responses

If you still get empty responses after fixing the message structure, 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 with your response."
        })
        # Retry
        response = client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    
    return response

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 sets stop_reason to "tool_use". The response content will contain one or more tool_use blocks.

Handling tool_use in Python

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "calculator",
        "description": "Performs arithmetic operations",
        "input_schema": {
            "type": "object",
            "properties": {
                "operation": {"type": "string"},
                "a": {"type": "number"},
                "b": {"type": "number"}
            },
            "required": ["operation", "a", "b"]
        }
    }],
    messages=[{"role": "user", "content": "What's 1234 + 5678?"}]
)

if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": # Execute the tool and get result result = execute_tool(block.name, block.input) # Append tool result and continue the conversation messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "content": str(result) }] }) # Make another API call to get Claude's final response final_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages )

Important: After handling the tool call, you must send the tool result back to Claude in a new user message. Claude will then continue its response, potentially calling more tools or finishing with end_turn.

max_tokens: Response Was Truncated

If Claude reaches the max_tokens limit you set, it stops with stop_reason: "max_tokens". This means the response is incomplete.

Handling max_tokens

if response.stop_reason == "max_tokens":
    # The response was cut off. Continue the conversation.
    messages.append({"role": "assistant", "content": response.content})
    messages.append({
        "role": "user",
        "content": "Please continue from where you left off."
    })
    
    continued_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,  # Consider increasing the limit
        messages=messages
    )
Best practice: If you frequently hit max_tokens, consider increasing the limit or breaking your request into smaller chunks.

stop_sequence: Custom Stop Triggered

If you provided a stop_sequences parameter in your API request, Claude will stop when it encounters one of those sequences. The stop_reason will be "stop_sequence", and the stop_sequence field will contain the matched sequence.

Example with stop_sequences

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

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # The content will not include the stop sequence

Building a Robust Response Handler

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

def handle_claude_response(client, messages, tools=None):
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages,
            tools=tools
        )
        
        if response.stop_reason == "end_turn":
            # Natural completion
            if response.content:
                return response.content[0].text
            else:
                # Empty response - add continuation prompt
                messages.append({"role": "user", "content": "Please continue."})
                continue
        
        elif response.stop_reason == "tool_use":
            # Handle tool calls
            messages.append({"role": "assistant", "content": response.content})
            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  # Let Claude respond to tool results
        
        elif response.stop_reason == "max_tokens":
            # Response truncated - continue
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
            continue
        
        elif response.stop_reason == "stop_sequence":
            # Custom stop sequence encountered
            return response.content[0].text

Best Practices Summary

  • Always check stop_reason – Don't assume the response is complete. Handle each reason appropriately.
  • Avoid adding text after tool results – Send only the tool_result block to prevent empty responses.
  • Use continuation prompts for max_tokens – Append a "Please continue" message to get the rest of the response.
  • Handle empty end_turn responses – Add a new user message to prompt Claude to continue.
  • Log stop reasons – In production, log stop_reason for debugging and monitoring.

Key Takeaways

  • stop_reason tells you why Claude stoppedend_turn (natural), tool_use (wants to call a tool), max_tokens (truncated), or stop_sequence (custom trigger).
  • Empty responses with end_turn are common in tool-use flows – Prevent them by sending only tool_result blocks without extra text.
  • tool_use requires you to execute the tool and send results back – Always append the tool result in a new user message to continue the conversation.
  • max_tokens means the response is incomplete – Use a continuation prompt and consider increasing the token limit.
  • Build a loop that handles all stop reasons – A robust handler checks stop_reason and reacts accordingly, ensuring smooth multi-turn interactions.