Mastering Claude API Stop Reasons: Build Robust Applications with end_turn, max_tokens & tool_use
Learn how to handle Claude API stop_reason values (end_turn, max_tokens, tool_use, stop_sequence) to build reliable AI applications with proper error handling and response processing.
This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and shows how to handle each case in your code to prevent empty responses, manage tool calls, and build robust applications.
Introduction
When you send a request to the Claude API, the response includes a stop_reason field that tells you why the model stopped generating. Understanding these values is essential for building applications that gracefully handle different scenarios—whether Claude finished naturally, hit a token limit, or wants to use a tool.
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 production applications
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
1. end_turn
The most common stop reason. Claude finished its response naturally—it decided the conversation turn was complete.
When to expect it: Standard Q&A, simple instructions, or after Claude has provided a complete answer. 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. max_tokens
Claude stopped because it reached the max_tokens limit you set. The response is truncated—you're only seeing part of what Claude wanted to say.
max_tokens is set too low.
How to handle it:
if response.stop_reason == "max_tokens":
# The response is incomplete. Send it back to Claude to continue.
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=1024,
messages=messages
)
Pro tip: Increase max_tokens if you frequently hit this limit. For very long outputs, implement a loop that continues until you get end_turn.
3. tool_use
Claude wants to call a tool (function) you've provided. The response contains one or more tool_use content blocks.
if response.stop_reason == "tool_use":
# Extract tool calls from the response
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
tool_use_id = block.id
# Execute the tool (your implementation)
result = execute_tool(tool_name, tool_input)
# Add tool result to messages
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": str(result)
}]
})
# Continue the conversation
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
4. stop_sequence
Claude stopped because it encountered a custom stop sequence you defined in your API request.
When to expect it: When you've setstop_sequences (e.g., ["\n\nHuman:"]) and Claude generates that sequence.
How to handle it:
if response.stop_reason == "stop_sequence":
# The response was cut off at the stop sequence
# You may want to trim the stop sequence from the output
text = response.content[0].text
# Remove the stop sequence if it's included
for seq in stop_sequences:
if text.endswith(seq):
text = text[:-len(seq)]
break
print(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, particularly 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
]}
]
Handling Empty Responses When They Occur
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
)
if response.stop_reason == "end_turn" and not response.content:
# ❌ Don't just retry with the empty response
# ✅ Add a continuation prompt in a NEW user message
messages.append({
"role": "user",
"content": "Please continue with your response."
})
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
Building a Robust Response Handler
Here's a complete handler that manages all stop reasons:
from anthropic import Anthropic
client = Anthropic()
def process_response(response, messages, tool_executor):
"""Handle all stop_reason cases and return final response."""
if response.stop_reason == "end_turn":
if not response.content:
# Empty response - prompt 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
elif response.stop_reason == "max_tokens":
# Truncated - ask to continue
messages.append({
"role": "assistant",
"content": response.content
})
messages.append({
"role": "user",
"content": "Please continue from where you left off."
})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
elif response.stop_reason == "tool_use":
# Execute tools and continue
messages.append({
"role": "assistant",
"content": response.content
})
for block in response.content:
if block.type == "tool_use":
result = tool_executor(block.name, block.input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
elif response.stop_reason == "stop_sequence":
# Custom stop sequence hit
return response
else:
raise ValueError(f"Unknown stop_reason: {response.stop_reason}")
Usage
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
final_response = process_response(response, messages, my_tool_executor)
Best Practices
- Always check
stop_reason– Never assume a response is complete without checking. - Handle
max_tokensgracefully – Implement a loop that continues until you getend_turn. - Don't add text after
tool_result– This prevents empty responses. - Use continuation prompts for empty responses – Never resend the same empty response.
- Log
stop_reasonfor debugging – Track which stop reasons occur most frequently in your application.
Key Takeaways
- Four stop reasons exist:
end_turn(natural completion),max_tokens(truncated),tool_use(wants to call a tool), andstop_sequence(custom stop triggered). - Empty responses with
end_turnare usually caused by adding text aftertool_resultblocks—send tool results alone. - For
max_tokens, never assume the response is complete; implement a continuation loop. - For
tool_use, execute the tool, add the result as atool_resultblock, and continue the conversation. - Always check
stop_reasonin production code to handle each case appropriately and build robust applications.