Mastering Claude's Stop Reasons: A Practical Guide to Handling API Responses
Learn how to interpret and handle Claude's stop_reason field in the Messages API, including end_turn, max_tokens, tool_use, and error handling strategies.
This guide explains Claude's stop_reason field—why the model stops generating—and how to handle each value (end_turn, max_tokens, tool_use, stop_sequence) in your API applications. You'll learn to detect empty responses, manage tool calls, and build robust conversational loops.
Mastering Claude's Stop Reasons: A Practical Guide to Handling API Responses
Every time you call Claude's Messages API, the response includes a stop_reason field. This small but critical piece of data tells you why the model stopped generating—whether it finished naturally, hit a token limit, or wants to use a tool. Understanding and correctly handling these stop reasons is essential for building reliable, production-ready applications.
In this guide, we'll explore each stop reason value, show you how to handle them in code, and share best practices to avoid common pitfalls like empty responses.
What Is stop_reason?
The stop_reason field is part of every successful Messages API response. Unlike error codes (which indicate failures), stop_reason tells you why Claude successfully completed its response generation. It's your signal for what to do next.
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
}
}
The stop_reason can be one of four values: end_turn, max_tokens, tool_use, or stop_sequence. Let's dive into each.
The Four Stop Reasons
1. end_turn – Natural Completion
This is the most common stop reason. It means Claude finished its response naturally—it decided the conversation turn was complete and voluntarily stopped.
When to expect it: After Claude answers a question, provides information, or completes a task without needing tools. How to handle it: Simply process the response content and wait for the next user input.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)
#### The Empty Response Gotcha
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's turn is complete, particularly after tool results.
- Adding text blocks immediately after tool results in the message array
- Sending Claude's completed response back without adding anything new
# 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, use a continuation prompt:
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 in a NEW user message
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
2. max_tokens – Token Limit Reached
This stop reason means Claude reached the max_tokens limit you set in your API request before it could finish its thought.
max_tokens value.
How to handle it: You need to continue the conversation by sending Claude's response back along with a prompt to continue.
def handle_max_tokens(client, messages, response):
if response.stop_reason == "max_tokens":
# Append Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
# Ask Claude to continue
messages.append({"role": "user", "content": "Please continue"})
# Make a new request
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Best practice: Set max_tokens generously (e.g., 4096 or higher) to minimize interruptions, especially for complex tasks.
3. tool_use – Claude Wants to Use a Tool
This stop reason indicates Claude decided to call one or more tools. The response content array will contain tool_use content blocks.
def handle_tool_use(client, messages, response):
if response.stop_reason == "tool_use":
# Append Claude's response (with tool_use blocks) to messages
messages.append({"role": "assistant", "content": response.content})
# Process each tool call
tool_results = []
for block in response.content:
if block.type == "tool_use":
# Execute the tool (pseudocode)
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
# Send results back to Claude
messages.append({"role": "user", "content": tool_results})
# Get Claude's next response
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Important: After sending tool results, Claude may respond with another tool_use, max_tokens, or end_turn. Your code should loop until you get end_turn.
4. stop_sequence – Custom Stop Sequence Triggered
This stop reason means Claude encountered a custom stop sequence you defined in your API request.
When to expect it: When you setstop_sequences in your request (e.g., ["\n\nHuman:"]).
How to handle it: The response is complete up to the stop sequence. Process it as a finished response.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\nHuman:"],
messages=[{"role": "user", "content": "Tell me a short story"}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
print(response.content[0].text)
Building a Robust Response Handler
Here's a complete loop that handles all stop reasons gracefully:
from anthropic import Anthropic
client = Anthropic()
def process_conversation(messages, max_iterations=10):
"""
Process a conversation, handling all stop reasons.
Returns the final response when Claude finishes naturally.
"""
for i in range(max_iterations):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=messages
)
if response.stop_reason == "end_turn":
# Check for empty response
if not response.content:
messages.append({"role": "user", "content": "Please continue"})
continue
return response
elif response.stop_reason == "max_tokens":
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue"})
elif response.stop_reason == "tool_use":
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})
elif response.stop_reason == "stop_sequence":
return response
raise Exception("Max iterations reached without end_turn")
Common Pitfalls and Solutions
| Pitfall | Solution |
|---|---|
Empty response with end_turn | Use a continuation prompt or fix message structure |
Infinite loop with tool_use | Set a maximum iteration limit |
Truncated responses with max_tokens | Increase max_tokens or implement continuation logic |
Ignoring stop_sequence | Check for it and handle appropriately |
Key Takeaways
stop_reasontells you why Claude stopped – it's not an error, but a signal for your next actionend_turnmeans Claude finished naturally; watch for empty responses and use continuation prompts if neededmax_tokensrequires you to continue the conversation by sending Claude's response back with a "Please continue" prompttool_usemeans you need to execute the tool and send results back; loop until you getend_turn- Always check
stop_reasonin your API handler to build robust, production-ready applications