Mastering Claude’s Stop Reasons: Build Reliable API Applications
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 for robust app logic.
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 code. You’ll also learn to prevent and recover from empty responses when using tools.
Introduction
Every time you call the Claude Messages API, the response includes a stop_reason field. This small piece of data tells you why Claude stopped generating—whether it finished naturally, requested a tool call, hit a token limit, or encountered a custom stop sequence. Understanding these reasons 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 recover from empty responses
- Best practices for tool-using agents
The stop_reason Field
The stop_reason field appears in every successful API response. Unlike errors, which indicate a failure, stop_reason tells you why Claude completed its response normally.
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
end_turn
This is the most common stop reason. It means Claude finished its response naturally—the model decided the assistant’s turn was complete.
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)
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
)
max_tokens
Claude reached the max_tokens limit you set. The response may be cut off mid-sentence. You should continue the conversation to get the rest.
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=1024,
messages=messages
)
stop_sequence
Claude encountered a custom stop sequence you defined in the request (e.g., "\n\nHuman:"). The stop_sequence field will contain the actual sequence that triggered the stop.
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# Process the response up to that point
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 in tool-use scenarios.
Common Causes
- Adding text 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 unchanged – Claude already decided it’s done, so it remains done.
How to Prevent Empty Responses
Incorrect: Adding text immediately aftertool_result:
messages = [
{"role": "user", "content": "Calculate 1234 + 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: Send only the tool result:
messages = [
{"role": "user", "content": "Calculate 1234 + 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
]}
]
Recovering from Empty Responses
If you still get empty responses after fixing the above, don’t just retry with the same messages—Claude has already decided it’s done. Instead, add a continuation prompt:
def handle_empty_response(client, messages):
response = client.messages.create(
model="claude-opus-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn" and not response.content:
# ❌ Don't just retry with the same messages
# ✅ Add a continuation prompt in a NEW user message
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Best Practices for Handling Stop Reasons
- Always check
stop_reason– Don’t assume the response is complete. Check the reason before processing. - Use a loop for tool calls – Claude may call multiple tools in sequence. Keep the conversation going until you get
end_turn. - Handle
max_tokensgracefully – Always continue the conversation when you hit the token limit. - Avoid extra text after tool results – Stick to the
tool_resultblock alone to prevent empty responses. - Log
stop_sequencefor debugging – If you use custom stop sequences, log which one triggered to understand Claude’s behavior.
Full Example: Tool-Using Agent Loop
Here’s a complete Python example that handles all stop reasons in a tool-using agent:
from anthropic import Anthropic
client = Anthropic()
messages = [{"role": "user", "content": "What's the weather in London and New York?"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn":
# Final response
print(response.content[0].text)
break
elif response.stop_reason == "tool_use":
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)}
]})
elif response.stop_reason == "max_tokens":
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
elif response.stop_reason == "stop_sequence":
print(f"Hit stop sequence: {response.stop_sequence}")
break
Conclusion
Understanding stop_reason is essential for building robust Claude API applications. By handling each reason correctly—especially tool_use and max_tokens—you can create seamless conversational experiences that handle tool calls, long responses, and edge cases like empty responses.
Key Takeaways
end_turnmeans Claude finished naturally; process the response as complete.tool_usemeans Claude wants to call a tool; execute it and return the result.max_tokensmeans you hit the token limit; continue the conversation to get the rest.stop_sequencemeans a custom stop sequence was triggered; check thestop_sequencefield.- Prevent empty responses by sending only
tool_resultblocks without extra text, and recover by adding a continuation prompt.