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.
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.
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_reasonvalue 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.
- 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)
# 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.
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 thestop_reasonfield 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_turnresponses: Especially in tool-using applications, be prepared for empty responses and implement continuation logic.
- Use
stop_sequencesfor 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_reasonandstop_sequencevalues 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_reasonin streaming: When using streaming, check thestop_reasonin 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), andstop_sequence(custom delimiter encountered). - Always check
stop_reason: Don't assume the response is complete—handle each reason appropriately. - Handle empty
end_turnresponses: 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.