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. Practical code examples and troubleshooting tips included.
This guide explains the stop_reason field in Claude's API responses, covering each value (end_turn, tool_use, max_tokens, stop_sequence) with code examples. You'll learn how to handle empty responses, continue conversations after tool calls, and build robust multi-turn interactions.
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 critical piece of data tells you exactly why Claude ended its response—whether it finished naturally, requested to use a tool, hit a token limit, or encountered a stop sequence.
Understanding stop_reason is essential for building reliable, production-ready applications. Without it, you might miss a tool call, cut off a response prematurely, or get stuck in infinite loops. In this guide, we'll break down each stop reason, show you how to handle them in code, and share best practices to avoid common pitfalls.
What Is the stop_reason Field?
The stop_reason field is part of every successful response from the Messages API. It's not an error—it's a signal that tells you the model finished generating for a specific reason. 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 possible values for stop_reason are:
end_turn– Claude finished its response naturally.tool_use– Claude wants to call a tool (function).max_tokens– The response was cut off because it reached themax_tokenslimit.stop_sequence– A custom stop sequence you provided was encountered.
end_turn: The Natural Stop
end_turn is the most common stop reason. It means Claude decided it had completed its response and voluntarily stopped. This is the ideal outcome for simple Q&A or single-turn interactions.
Handling end_turn in 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":
# 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 in tool-use scenarios when:
- You add text blocks immediately after
tool_resultblocks. - You send Claude's completed response back without adding anything new.
#### How to Prevent Empty Responses
Incorrect approach: Adding text after tool results.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 do this
]}
]
Correct approach: Send only the 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"} # ✅ Just the result
]}
]
#### Handling Persistent Empty Responses
If you still get empty responses after fixing the message structure, 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 with your response."
})
# Retry
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
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 sets stop_reason to "tool_use". The response content will contain one or more tool_use blocks.
Handling tool_use in Python
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "calculator",
"description": "Performs 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's 1234 + 5678?"}]
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute the tool and get result
result = execute_tool(block.name, block.input)
# Append tool result and continue the conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
# Make another API call to get Claude's final response
final_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
Important: After handling the tool call, you must send the tool result back to Claude in a new user message. Claude will then continue its response, potentially calling more tools or finishing with end_turn.
max_tokens: Response Was Truncated
If Claude reaches the max_tokens limit you set, it stops with stop_reason: "max_tokens". This means the response is incomplete.
Handling max_tokens
if response.stop_reason == "max_tokens":
# The response was cut off. Continue the conversation.
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": "Please continue from where you left off."
})
continued_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048, # Consider increasing the limit
messages=messages
)
Best practice: If you frequently hit max_tokens, consider increasing the limit or breaking your request into smaller chunks.
stop_sequence: Custom Stop Triggered
If you provided a stop_sequences parameter in your API request, Claude will stop when it encounters one of those sequences. The stop_reason will be "stop_sequence", and the stop_sequence field will contain the matched sequence.
Example with stop_sequences
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\nHuman:"],
messages=[{"role": "user", "content": "Tell me a story and then stop before saying 'Human:'"}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# The content will not include the stop sequence
Building a Robust Response Handler
Here's a complete example that handles all stop reasons gracefully:
def handle_claude_response(client, messages, tools=None):
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=tools
)
if response.stop_reason == "end_turn":
# Natural completion
if response.content:
return response.content[0].text
else:
# Empty response - add continuation prompt
messages.append({"role": "user", "content": "Please continue."})
continue
elif response.stop_reason == "tool_use":
# Handle tool calls
messages.append({"role": "assistant", "content": response.content})
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)
}]
})
continue # Let Claude respond to tool results
elif response.stop_reason == "max_tokens":
# Response truncated - continue
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
continue
elif response.stop_reason == "stop_sequence":
# Custom stop sequence encountered
return response.content[0].text
Best Practices Summary
- Always check
stop_reason– Don't assume the response is complete. Handle each reason appropriately. - Avoid adding text after tool results – Send only the
tool_resultblock to prevent empty responses. - Use continuation prompts for
max_tokens– Append a "Please continue" message to get the rest of the response. - Handle empty
end_turnresponses – Add a new user message to prompt Claude to continue. - Log stop reasons – In production, log
stop_reasonfor debugging and monitoring.
Key Takeaways
stop_reasontells you why Claude stopped –end_turn(natural),tool_use(wants to call a tool),max_tokens(truncated), orstop_sequence(custom trigger).- Empty responses with
end_turnare common in tool-use flows – Prevent them by sending onlytool_resultblocks without extra text. tool_userequires you to execute the tool and send results back – Always append the tool result in a new user message to continue the conversation.max_tokensmeans the response is incomplete – Use a continuation prompt and consider increasing the token limit.- Build a loop that handles all stop reasons – A robust handler checks
stop_reasonand reacts accordingly, ensuring smooth multi-turn interactions.