BeClaude
GuideBeginnerAgents2026-05-22

Mastering Claude API Stop Reasons: Build Robust Applications with end_turn, max_tokens & tool_use

Learn how to handle Claude API stop_reason values (end_turn, max_tokens, tool_use, stop_sequence) to build reliable AI applications with proper error handling and response processing.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and shows how to handle each case in your code to prevent empty responses, manage tool calls, and build robust applications.

Claude APIstop_reasonerror handlingtool useresponse processing

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. Understanding these values is essential for building applications that gracefully handle different scenarios—whether Claude finished naturally, hit a token limit, or wants to use a tool.

In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle them in Python and TypeScript
  • How to prevent and fix empty responses
  • Best practices for production applications

The stop_reason Field

The stop_reason field is part of every successful Messages API response. Unlike errors (which indicate failures), stop_reason tells you why Claude successfully completed its response generation.

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

1. end_turn

The most common stop reason. Claude finished its response naturally—it decided the conversation turn was complete.

When to expect it: Standard Q&A, simple instructions, or after Claude has provided a complete answer. How to handle it:
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)

2. max_tokens

Claude stopped because it reached the max_tokens limit you set. The response is truncated—you're only seeing part of what Claude wanted to say.

When to expect it: Long responses, complex code generation, or when max_tokens is set too low. How to handle it:
if response.stop_reason == "max_tokens":
    # The response is incomplete. Send it back to Claude to continue.
    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=1024,
        messages=messages
    )
Pro tip: Increase max_tokens if you frequently hit this limit. For very long outputs, implement a loop that continues until you get end_turn.

3. tool_use

Claude wants to call a tool (function) you've provided. The response contains one or more tool_use content blocks.

When to expect it: When you've defined tools and Claude decides it needs external data or computation. How to handle it:
if response.stop_reason == "tool_use":
    # Extract tool calls from the response
    for block in response.content:
        if block.type == "tool_use":
            tool_name = block.name
            tool_input = block.input
            tool_use_id = block.id
            
            # Execute the tool (your implementation)
            result = execute_tool(tool_name, tool_input)
            
            # Add tool result to messages
            messages.append({"role": "assistant", "content": response.content})
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool_use_id,
                    "content": str(result)
                }]
            })
    
    # Continue the conversation
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

4. stop_sequence

Claude stopped because it encountered a custom stop sequence you defined in your API request.

When to expect it: When you've set stop_sequences (e.g., ["\n\nHuman:"]) and Claude generates that sequence. How to handle it:
if response.stop_reason == "stop_sequence":
    # The response was cut off at the stop sequence
    # You may want to trim the stop sequence from the output
    text = response.content[0].text
    # Remove the stop sequence if it's included
    for seq in stop_sequences:
        if text.endswith(seq):
            text = text[:-len(seq)]
            break
    print(text)

Handling Empty Responses with end_turn

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 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 approach:
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 approach:
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
    ]}
]

Handling Empty Responses When They Occur

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
    )
    
    if response.stop_reason == "end_turn" and not response.content:
        # ❌ Don't just retry with the empty response
        # ✅ Add a continuation prompt in a NEW user message
        messages.append({
            "role": "user",
            "content": "Please continue with your response."
        })
        
        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 manages all stop reasons:

from anthropic import Anthropic

client = Anthropic()

def process_response(response, messages, tool_executor): """Handle all stop_reason cases and return final response.""" if response.stop_reason == "end_turn": if not response.content: # Empty response - prompt 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 elif response.stop_reason == "max_tokens": # Truncated - ask to continue messages.append({ "role": "assistant", "content": response.content }) messages.append({ "role": "user", "content": "Please continue from where you left off." }) return client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages ) elif response.stop_reason == "tool_use": # Execute tools and continue messages.append({ "role": "assistant", "content": response.content }) for block in response.content: if block.type == "tool_use": result = tool_executor(block.name, block.input) 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": # Custom stop sequence hit return response else: raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Usage

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

final_response = process_response(response, messages, my_tool_executor)

Best Practices

  • Always check stop_reason – Never assume a response is complete without checking.
  • Handle max_tokens gracefully – Implement a loop that continues until you get end_turn.
  • Don't add text after tool_result – This prevents empty responses.
  • Use continuation prompts for empty responses – Never resend the same empty response.
  • Log stop_reason for debugging – Track which stop reasons occur most frequently in your application.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), max_tokens (truncated), tool_use (wants to call a tool), and stop_sequence (custom stop triggered).
  • Empty responses with end_turn are usually caused by adding text after tool_result blocks—send tool results alone.
  • For max_tokens, never assume the response is complete; implement a continuation loop.
  • For tool_use, execute the tool, add the result as a tool_result block, and continue the conversation.
  • Always check stop_reason in production code to handle each case appropriately and build robust applications.