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 code examples, troubleshooting tips, and best practices for robust app development.
This guide explains the stop_reason field in Claude API responses, covering values like end_turn, tool_use, and max_tokens. You'll learn how to handle each case with practical Python code, prevent empty responses, and build more robust conversational applications.
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. This isn't an error—it's a signal. Understanding these signals is essential for building applications that respond intelligently, whether you're handling a simple Q&A bot, a multi-step tool-using agent, or a long-running conversation.
In this guide, we'll break down each stop_reason value, show you how to handle them in code, and share best practices to avoid common pitfalls like empty responses.
The stop_reason Field
The stop_reason field appears in every successful response from the Messages API. 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
end_turn
What it means: Claude finished its response naturally. This is the most common stop reason and usually indicates a complete, final answer. How to handle it: In most cases, you can simply display the response to the user. However, be aware of the empty response edge case (covered below).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)
tool_use
What it means: Claude wants to call a tool. The response content will contain one or moretool_use blocks. Your application must execute the tool and return the result.
How to handle it: Loop through response.content, identify tool_use blocks, execute the corresponding functions, and append the results as new tool_result messages.
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# Execute your tool logic here
result = execute_tool(tool_name, tool_input)
# Append tool_result to messages
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
# Continue the conversation
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
max_tokens
What it means: Claude hit the token limit you set. The response may be cut off mid-sentence or mid-thought. How to handle it: You have two options:- Increase
max_tokensin your request. - Send the partial response back as a new user message with a continuation prompt like "Please continue."
if response.stop_reason == "max_tokens":
# Option 1: Increase max_tokens and retry
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096, # Increased limit
messages=messages
)
# Option 2: Ask Claude to continue
messages.append({
"role": "user",
"content": "Please continue from where you left off."
})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
stop_sequence
What it means: Claude encountered a custom stop sequence you defined in your request. This is useful for structured outputs where you want to stop generation at a specific marker. How to handle it: Check thestop_sequence field to see which sequence was triggered, then process the response accordingly.
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# Process the response up to that point
content_filtered
What it means: Claude's response was filtered by content moderation. The response content will be empty or truncated. How to handle it: Log the event for review, and consider rephrasing the user's input to avoid triggering filters.if response.stop_reason == "content_filtered":
print("Response was filtered. Consider rephrasing the input.")
# Optionally, ask the user to rephrase
Handling Empty Responses with end_turn
A common gotcha: Claude returns an empty response (2-3 tokens, no content) with stop_reason: "end_turn". This typically happens after tool results, when Claude interprets that the assistant's turn is complete.
Why It Happens
- Adding text blocks immediately after
tool_resultmessages (Claude learns to expect the user to always insert text after tool results, so it ends its turn). - Sending Claude's completed response back without adding anything new (Claude already decided it's done).
How to Prevent It
Incorrect: Adding text after tool_resultmessages = [
{"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: 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
]}
]
Recovery Strategy
If you still get empty responses, don't just 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-7",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn" and not response.content:
# Add a continuation prompt
messages.append({"role": "user", "content": "Please continue"})
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 example that handles all stop reasons gracefully:
from anthropic import Anthropic
client = Anthropic()
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
def handle_response(response, messages):
if response.stop_reason == "end_turn":
if response.content:
return response.content[0].text
else:
# Empty response recovery
messages.append({"role": "user", "content": "Please continue"})
new_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return handle_response(new_response, messages)
elif response.stop_reason == "tool_use":
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)
}]
})
new_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return handle_response(new_response, messages)
elif response.stop_reason == "max_tokens":
messages.append({"role": "user", "content": "Please continue"})
new_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=messages
)
return handle_response(new_response, messages)
elif response.stop_reason == "stop_sequence":
return response.content[0].text if response.content else ""
elif response.stop_reason == "content_filtered":
return "Response was filtered. Please rephrase your question."
return "Unknown stop reason"
Usage
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
final_answer = handle_response(response, messages)
print(final_answer)
Key Takeaways
- Understand each stop reason:
end_turnmeans natural completion,tool_usemeans Claude wants to call a tool,max_tokensmeans you hit the limit,stop_sequencemeans a custom stop was triggered, andcontent_filteredmeans moderation intervened. - Handle empty responses gracefully: Never add text blocks after
tool_resultmessages, and use a continuation prompt (not a retry) to recover from emptyend_turnresponses. - Build a recursive handler: A single function that processes all stop reasons and loops back for
tool_useandmax_tokenscreates a robust, production-ready pipeline. - Log and monitor: Always log
stop_reasonandstop_sequencevalues during development to catch unexpected behavior early. - Test edge cases: Simulate empty responses, token limits, and tool calls to ensure your handler behaves correctly under all conditions.