BeClaude
Guide2026-05-06

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. Includes practical code examples, troubleshooting tips for empty responses, and best practices for robust app logic.

Quick Answer

This guide explains every Claude API stop_reason value (end_turn, tool_use, max_tokens, stop_sequence) and shows you how to handle them correctly in your code. You'll also learn how to prevent and recover from empty responses.

Claude APIstop_reasonerror handlingtool useAPI best practices

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 mighty piece of data tells you exactly why Claude finished its response — whether it completed naturally, called a tool, hit a token limit, or encountered a custom stop sequence.

Understanding stop_reason is essential for building robust, production-ready applications. Without it, you might miss a tool call, truncate a response, or get stuck in an infinite loop. In this guide, you'll learn:

  • What each stop_reason value means
  • How to handle them in Python and TypeScript
  • How to prevent and recover from empty responses
  • Best practices for chaining multi-turn conversations
Let's dive in.

The stop_reason Field

Every successful response from the Messages API includes a stop_reason field. It's not an error — it's a signal. Here's a typical response:

{
  "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
  }
}

There are four possible values for stop_reason:

stop_reasonMeaning
end_turnClaude finished its response naturally. No further action needed — the model has said everything it wants to say.
tool_useClaude wants to call a tool. You must execute the tool and return the result before continuing.
max_tokensClaude hit the max_tokens limit you set. The response is truncated. You should continue the conversation to get the rest.
stop_sequenceClaude encountered a custom stop sequence you defined in the API request.

Handling Each Stop Reason

1. end_turn — Natural Completion

This is the most common and simplest case. Claude has finished its response and expects no further action.

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": print(response.content[0].text)

TypeScript:
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); }

2. 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 stops with stop_reason: "tool_use". Your application must:

  • Extract the tool call details from the response content.
  • Execute the tool.
  • Send the tool result back in a new user message.
Python example:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "calculator",
        "description": "Perform 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 is 1234 + 5678?"}]
)

if response.stop_reason == "tool_use": tool_call = response.content[0] # Execute the tool (in real code, call your function) result = execute_tool(tool_call.name, tool_call.input) # Send the result back messages = [ {"role": "user", "content": "What is 1234 + 5678?"}, {"role": "assistant", "content": [tool_call]}, {"role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_call.id, "content": str(result)} ]} ] response = client.messages.create(model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages)

3. max_tokens — Truncated Response

If Claude hits the max_tokens limit, the response is cut off. You should continue the conversation to get the remaining content.

Python:
if response.stop_reason == "max_tokens":
    # Append Claude's partial response and ask 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)

4. stop_sequence — Custom Stop

If you defined a stop_sequences parameter (e.g., ["END"]), Claude stops when it encounters that sequence. The stop_sequence field tells you which one was hit.

Python:
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["END"],
    messages=[{"role": "user", "content": "List three colors and then write END."}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # The content will include everything before the stop sequence print(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 scenarios when:

  • You add text blocks immediately after tool_result blocks.
  • You send Claude's completed response back without adding anything new.

Why Does This Happen?

Claude learns from the conversation pattern. If you always insert text after tool results, Claude expects that pattern and ends its turn early. Similarly, if you send back a response where Claude already decided it's done, it will remain "done."

How to Prevent Empty Responses

Incorrect — adding text 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 here
    ]}
]
Correct — send tool results directly:
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
    ]}
]

Recovering from Empty Responses

If you still get empty responses after fixing the above, do not retry with the same messages — Claude has already decided it's done. Instead, add a continuation prompt in a new user message:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-20250514",
        max_tokens=1024,
        messages=messages
    )
    
    if response.stop_reason == "end_turn" and not response.content:
        # ✅ Correct: Add a continuation prompt
        messages.append({"role": "user", "content": "Please continue"})
        response = client.messages.create(
            model="claude-opus-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response

Building a Robust Stop Reason Handler

Here's a complete pattern you can use in production:

def handle_response(client, messages, tools=None):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages,
        tools=tools
    )
    
    if response.stop_reason == "end_turn":
        if response.content:
            return response.content[0].text
        else:
            # Empty response — prompt to continue
            messages.append({"role": "user", "content": "Please continue"})
            return handle_response(client, messages, tools)
    
    elif response.stop_reason == "tool_use":
        tool_call = response.content[0]
        result = execute_tool(tool_call.name, tool_call.input)
        messages.append({"role": "assistant", "content": [tool_call]})
        messages.append({"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": tool_call.id, "content": str(result)}
        ]})
        return handle_response(client, messages, tools)
    
    elif response.stop_reason == "max_tokens":
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": "Please continue."})
        return handle_response(client, messages, tools)
    
    elif response.stop_reason == "stop_sequence":
        return response.content[0].text

Key Takeaways

  • stop_reason is your app's compass — it tells you exactly why Claude stopped, so you can respond appropriately.
  • Handle tool_use by executing the tool and returning the result in a new user message with a tool_result block.
  • For max_tokens, continue the conversation by appending Claude's partial response and asking it to continue.
  • Prevent empty responses by never adding text blocks immediately after tool_result blocks, and always sending fresh context when retrying.
  • Build a recursive handler that checks stop_reason and loops until you get a complete end_turn response with content.