Mastering Claude API Stop Reasons: Build Robust Applications That Handle Every Response Type
Learn how to interpret and handle Claude's stop_reason field—end_turn, max_tokens, stop_sequence, and tool_use—to build reliable, production-ready applications with the Claude API.
This guide explains Claude's stop_reason field—end_turn, max_tokens, stop_sequence, and tool_use—and shows you how to handle each case with practical code examples to prevent empty responses, manage tool loops, and build robust applications.
Mastering Claude API Stop Reasons: Build Robust Applications That Handle Every Response Type
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 reliable applications—whether you're creating a simple chatbot, a complex agent with tool use, or a streaming interface.
In this guide, you'll learn:
- What each
stop_reasonvalue means - How to handle each case in Python and TypeScript
- How to prevent and fix empty responses
- Best practices for production applications
What Is the stop_reason Field?
The stop_reason field 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
}
}
The Four Stop Reason Values
1. end_turn — Natural Completion
What it means: Claude finished its response naturally. This is the most common stop reason and usually indicates a complete, satisfactory answer.
How to handle it: Process the response content directly.
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 content array with stop_reason: "end_turn". This typically happens in tool-use scenarios when:
- You add 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.
- You send Claude's completed response back without adding anything — Claude already decided it's done, so it remains done.
# 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:
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 same messages
# This won't work because Claude already decided it's done
# ✅ 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
What it means: Claude hit the max_tokens limit you set. The response may be cut off mid-sentence or mid-thought.
How to handle it: Continue the conversation by sending the response back as part of the conversation history.
def handle_max_tokens(client, messages, response):
if response.stop_reason == "max_tokens":
# Add Claude's partial response to the conversation
messages.append({"role": "assistant", "content": response.content})
# Ask Claude to continue
messages.append({"role": "user", "content": "Please continue."})
# Get the continuation
continuation = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return continuation
return response
3. stop_sequence — Custom Stop Sequence Triggered
What it means: Claude encountered a custom stop sequence you defined in your request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter.
How to handle it: Extract the content up to the stop sequence and process it as needed.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "List three colors"}],
stop_sequences=["\n"] # Stop at the first newline
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
print(f"Content: {response.content[0].text}")
4. tool_use — Tool Call Requested
What it means: Claude wants to call a tool. The response content will contain one or more tool_use blocks. This is the foundation for building agents.
How to handle it: Execute the tool, then send the results back.
def handle_tool_use(client, messages, response):
if response.stop_reason == "tool_use":
# Add Claude's response (with tool_use blocks) to history
messages.append({"role": "assistant", "content": response.content})
# Process each tool call
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)
})
# Send tool results back
messages.append({"role": "user", "content": tool_results})
# Get Claude's response after tool use
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Building a Complete Response Handler
Here's a robust handler that manages all stop reasons in a loop:
def get_complete_response(client, messages, max_iterations=10):
"""
Handle all stop reasons and continue until we get a complete response.
"""
for _ in range(max_iterations):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn":
if response.content:
return response # Complete, non-empty response
else:
# Empty response - add continuation prompt
messages.append({"role": "user", "content": "Please continue."})
continue
elif response.stop_reason == "max_tokens":
# Add partial response and ask to 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 reached - return as-is
return response
elif response.stop_reason == "tool_use":
# Handle tool calls
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})
continue
raise Exception("Max iterations reached without complete response")
Best Practices for Production
- Always check
stop_reason— Never assume a response is complete. Always inspect thestop_reasonfield.
- Handle empty
end_turnresponses gracefully — Implement the continuation pattern shown above.
- Set reasonable
max_tokens— Too low a limit will cause frequentmax_tokensstops, increasing latency and cost.
- Log
stop_reasonfor debugging — Track which stop reasons occur most often in your application to fine-tune your implementation.
- Use
stop_sequencesfor structured output — When you need Claude to stop at a specific delimiter (e.g., JSON closing brace), define custom stop sequences.
Key Takeaways
- Four stop reasons exist:
end_turn(natural completion),max_tokens(token limit hit),stop_sequence(custom delimiter reached), andtool_use(tool call requested). - Empty
end_turnresponses occur in tool-use scenarios when you add text aftertool_resultblocks—send tool results directly without extra text. - For
max_tokens, continue the conversation by adding Claude's partial response to the message history and asking it to continue. - For
tool_use, execute the tool and send results back in atool_resultblock to continue the conversation. - Build a loop handler that processes all stop reasons to ensure your application always gets a complete, useful response.