BeClaude
GuideBeginnerBest Practices2026-05-21

Mastering Claude's Stop Reasons: A Practical Guide to Building Robust API Applications

Learn how to handle Claude's stop_reason field in the Messages API, including end_turn, max_tokens, tool_use, and stop_sequence, with code examples and best practices.

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 applications to build robust, production-ready integrations.

stop_reasonMessages APItool_useerror handlingClaude API

Introduction

When you send a request to Claude via the Messages API, the response includes a stop_reason field. This field tells you why Claude stopped generating its response. Understanding these values is essential for building robust applications that handle different response types appropriately.

Unlike errors—which indicate failures in processing your request—stop_reason tells you why Claude successfully completed its response generation. This distinction is crucial for debugging, implementing retry logic, and orchestrating multi-turn conversations or tool-using agents.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle them in Python and TypeScript
  • Common pitfalls and how to avoid them
  • Best practices for production applications

The stop_reason Field

The stop_reason field is part of every successful Messages API response. Here's a typical example:

{
  "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 values for stop_reason:

1. end_turn

The most common stop reason. Indicates Claude finished its response naturally. This is what you'll see in most standard conversations.

Python Example:
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)

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

const client = new Anthropic();

const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }] });

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

#### Empty Responses with end_turn

Sometimes Claude returns an empty response (exactly 2-3 tokens with no content) with stop_reason: "end_turn". This typically happens when Claude interprets that the assistant turn is complete, particularly 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: 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 after fixing the above:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages
    )
    
    # Check if response is empty
    if response.stop_reason == "end_turn" and not response.content:
        # INCORRECT: Don't just retry with the empty response
        # This won't work because Claude already decided it's done
        
        # CORRECT: Add a continuation prompt in a NEW user message
        messages.append({
            "role": "user",
            "content": "Please continue with your response."
        })
        return client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    
    return response

2. max_tokens

Indicates Claude reached the maximum token limit you specified in your request. The response is truncated.

How to handle it:
def handle_max_tokens(response, messages, client):
    if response.stop_reason == "max_tokens":
        # Add Claude's partial response to the conversation
        messages.append({"role": "assistant", "content": response.content})
        # Ask Claude to continue
        messages.append({"role": "user", "content": "Please continue."})
        
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response
Best practice: Set max_tokens generously for open-ended tasks, or implement automatic retry logic with a continuation prompt.

3. tool_use

Claude wants to use a tool you've provided. The response will contain one or more tool_use content blocks.

How to handle it:
def handle_tool_use(response, messages, client, tools):
    if response.stop_reason == "tool_use":
        # Add Claude's response to the conversation
        messages.append({"role": "assistant", "content": response.content})
        
        # Process each tool use
        tool_results = []
        for content_block in response.content:
            if content_block.type == "tool_use":
                # Execute the tool (your implementation)
                result = execute_tool(content_block.name, content_block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": content_block.id,
                    "content": str(result)
                })
        
        # Send results back
        messages.append({"role": "user", "content": tool_results})
        
        return client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages,
            tools=tools
        )
    return response

4. stop_sequence

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

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

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

Building a Robust Handler

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

from anthropic import Anthropic

client = Anthropic()

def handle_claude_response(response, messages, tools=None): """Handle all possible stop reasons from Claude.""" if response.stop_reason == "end_turn": # Natural completion - process the response 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, tools=tools ) return response elif response.stop_reason == "max_tokens": # Truncated - ask to continue 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, tools=tools ) elif response.stop_reason == "tool_use": # Tool use requested 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}) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages, tools=tools ) elif response.stop_reason == "stop_sequence": # Custom stop sequence encountered print(f"Stopped at sequence: {response.stop_sequence}") return response else: # Unknown stop reason (shouldn't happen) raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Usage

messages = [{"role": "user", "content": "What's the weather in Paris?"}] response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages, tools=[weather_tool] )

final_response = handle_claude_response(response, messages, tools=[weather_tool])

Best Practices

  • Always check stop_reason: Don't assume the response is complete. Always check the stop_reason field to determine next steps.
  • Implement retry logic for max_tokens: When Claude hits the token limit, automatically continue the conversation rather than losing the partial response.
  • Handle empty end_turn responses: Especially in tool-using applications, be prepared for empty responses and implement continuation logic.
  • Use stop_sequences for structured output: When you need Claude to stop at a specific point (e.g., after generating a JSON object), use custom stop sequences.
  • Log stop reasons for debugging: In production, log the stop_reason and stop_sequence values to help diagnose issues.

Common Pitfalls

  • Not handling max_tokens: If you don't handle truncated responses, users may get incomplete answers.
  • Ignoring empty end_turn: This can cause infinite loops in tool-using agents.
  • Sending text after tool_result: This confuses Claude and leads to empty responses.
  • Not checking stop_reason in streaming: When using streaming, check the stop_reason in the final message event.

Key Takeaways

  • Claude returns four stop reasons: end_turn (natural completion), max_tokens (truncated), tool_use (wants to use a tool), and stop_sequence (custom delimiter encountered).
  • Always check stop_reason: Don't assume the response is complete—handle each reason appropriately.
  • Handle empty end_turn responses: Especially in tool-using workflows, implement continuation logic to avoid silent failures.
  • Use continuation prompts for max_tokens: When Claude hits the token limit, add a "Please continue" message to get the full response.
  • Log and monitor stop reasons: In production, tracking stop reasons helps you identify issues with token limits, tool configurations, or conversation patterns.