Mastering Claude API Stop Reasons: A Practical Guide to Handling end_turn, tool_use, and max_tokens
Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes code examples, empty response fixes, and best practices.
This guide explains Claude's stop_reason field — end_turn, tool_use, max_tokens, and stop_sequence — and shows how to handle each in your application. You'll learn to detect empty responses, continue tool-using conversations, and avoid common pitfalls.
Introduction
When you send a request to the Claude API, the response includes a stop_reason field. This field tells you why Claude stopped generating — whether it finished naturally, wants to use a tool, hit a token limit, or encountered a stop sequence. Understanding these values is essential for building reliable, production-ready applications.
In this guide, you'll learn:
- What each
stop_reasonvalue means - How to handle them in Python and TypeScript
- How to prevent and fix empty responses
- Best practices for tool-using conversations
The stop_reason Field
The stop_reason is part of every successful Messages API response. Unlike errors (which indicate a failure), 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 possible stop_reason values:
1. end_turn
The most common stop reason. Claude finished its response naturally and handed control back to the user. This is what you'll see for simple Q&A or when Claude has nothing more to say.
How to handle it: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)
2. tool_use
Claude wants to call a tool. The response will contain one or more tool_use content blocks. You must execute the tool and return the result in a new user message with a tool_result block.
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute the tool
result = execute_tool(block.name, block.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
)
3. max_tokens
Claude reached the max_tokens limit you set. The response is truncated. This often happens with long outputs or complex reasoning.
if response.stop_reason == "max_tokens":
# Append Claude's partial response
messages.append({"role": "assistant", "content": response.content})
# Ask Claude to continue
messages.append({"role": "user", "content": "Please continue."})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048, # Consider increasing
messages=messages
)
4. stop_sequence
Claude encountered a custom stop sequence you defined in your API request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter.
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": "Tell me a story."}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# The content may be partial; handle accordingly
print(response.content[0].text)
Handling 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, especially after tool results.
Common Causes
- 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.
How to Prevent Empty Responses
Incorrect approach: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 approach:
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 from Empty Responses
If you still get empty responses after fixing the above, use a continuation prompt:
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"})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Building a Robust Response Handler
Here's a complete handler that deals with all stop reasons:
from anthropic import Anthropic
client = Anthropic()
def handle_response(response, messages):
"""Process Claude's response and handle all stop reasons."""
if response.stop_reason == "end_turn":
if not response.content:
# Empty response recovery
messages.append({"role": "user", "content": "Please continue"})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
# Normal end_turn - conversation is complete
return response
elif response.stop_reason == "tool_use":
# Execute tools and continue
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 the conversation
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":
# Append partial response and ask to continue
messages.append({"role": "assistant", "content": response.content})
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":
# Custom stop sequence encountered
return response
else:
raise ValueError(f"Unknown stop_reason: {response.stop_reason}")
Best Practices
- Always check
stop_reason— Don't assume Claude finished naturally. Handletool_useandmax_tokensexplicitly. - Never add text after
tool_result— Keep tool results clean. Let Claude decide what to say. - Use continuation prompts for
max_tokens— Append Claude's partial output and ask it to continue. - Set appropriate
max_tokens— For complex tasks, start with 2048 or higher. - Log
stop_reasonfor debugging — It helps identify patterns in your application's behavior.
Key Takeaways
- Four stop reasons:
end_turn(natural finish),tool_use(wants to call a tool),max_tokens(output truncated), andstop_sequence(custom delimiter hit). - Handle
tool_useby executing the tool and returning results in atool_resultblock — never add extra text after it. - Recover from
max_tokensby appending Claude's partial response and asking it to continue with a higher limit. - Prevent empty
end_turnresponses by keeping tool results clean and using continuation prompts when needed. - Build a recursive handler that processes all stop reasons until you get a complete
end_turnresponse.