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 stop_sequence with practical code examples.
Claude's stop_reason field tells you why the model stopped generating—end_turn for natural completion, max_tokens for truncation, tool_use for tool calls, and stop_sequence for custom stops. This guide shows how to handle each case in your application.
Introduction
When you call the Claude Messages API, every successful response includes a stop_reason field. This field is your key to understanding why Claude stopped generating—whether it finished naturally, hit a token limit, needs to call a tool, or encountered a custom stop sequence. Misinterpreting these values leads to broken workflows, truncated responses, or infinite loops.
In this guide, you'll learn exactly what each stop_reason value means, how to handle them in Python and TypeScript, and how to avoid common pitfalls like empty responses and incomplete tool chains.
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
Claude can return four distinct stop_reason values. Each requires a different handling strategy.
end_turn — Natural Completion
What it means: Claude finished its response naturally and has nothing more to say. This is the most common stop reason and usually indicates a successful, complete response.
How to handle it: Simply process the response content. No further action is needed.
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)
#### ⚠️ 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.
- Adding text blocks immediately after
tool_resultblocks - 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, add a continuation prompt in a new user message:
def handle_empty_response(client, messages):
response = client.messages.create(
model="claude-sonnet-4-20250514",
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 with your response."
})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
max_tokens — Token Limit Reached
What it means: Claude stopped because it reached the max_tokens limit you set. The response is likely truncated—Claude had more to say but ran out of space.
How to handle it: You need to continue the conversation by sending Claude's response back along with a continuation prompt.
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})
# Add a continuation prompt
messages.append({"role": "user", "content": "Please continue."})
# Make a new request with higher max_tokens if needed
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048, # Increase limit
messages=messages
)
return response
Pro tip: If you're streaming responses, you can detect max_tokens early and automatically increase the limit for the next chunk.
tool_use — Tool Call Requested
What it means: Claude wants to call a tool (function). The response contains one or more tool_use content blocks. You must execute the tool and return results before Claude can continue.
How to handle it:
def handle_tool_use(client, messages, response):
if response.stop_reason == "tool_use":
# Append Claude's response to history
messages.append({"role": "assistant", "content": response.content})
# Process each tool use block
tool_results = []
for block in response.content:
if block.type == "tool_use":
# Execute the tool (your implementation)
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})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Important: Always append Claude's full response (including tool use blocks) to the message history before adding tool results. This maintains the conversation context.
stop_sequence — Custom Stop Sequence
What it means: Claude encountered one of your custom stop_sequences (defined in the API request) and stopped. This is useful for structured outputs or early termination.
How to handle it:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\nHuman:", "\n\nAssistant:"],
messages=[{"role": "user", "content": "List 3 colors."}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# Process the response up to the stop sequence
print(response.content[0].text)
Building a Robust Handler
Combine all four cases into a single handler for production applications:
def handle_claude_response(client, messages, response, max_iterations=10):
iteration = 0
while iteration < max_iterations:
iteration += 1
if response.stop_reason == "end_turn":
# Final response
return response.content[0].text if response.content else ""
elif response.stop_reason == "max_tokens":
# Continue generation
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=2048,
messages=messages
)
elif response.stop_reason == "tool_use":
# Execute tools
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})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
elif response.stop_reason == "stop_sequence":
# Custom stop reached
return response.content[0].text if response.content else ""
raise Exception("Max iterations reached without end_turn")
Common Pitfalls and Solutions
| Pitfall | Solution |
|---|---|
Empty response with end_turn | Add a continuation prompt in a new user message |
| Infinite tool call loops | Set a maximum iteration limit (e.g., 10) |
| Truncated responses | Increase max_tokens or implement continuation logic |
| Missing tool results | Always append Claude's full response before adding tool results |
Key Takeaways
end_turnmeans Claude finished naturally—process the response as-is, but watch for empty responses after tool calls.max_tokensmeans the response was truncated—send Claude's response back with a continuation prompt and consider increasingmax_tokens.tool_usemeans Claude needs to call a tool—execute the tool and return results in a new user message withtool_resultblocks.stop_sequencemeans a custom stop sequence was hit—handle it according to your application logic.- Always implement a maximum iteration limit when handling
tool_useormax_tokensto prevent infinite loops.