Mastering Claude’s Stop Reasons: A Practical Guide to Handling API Responses
Learn how to interpret and handle Claude's stop_reason field in the Messages API. Covers end_turn, tool_use, max_tokens, and empty response prevention with code examples.
This guide explains Claude's stop_reason field—end_turn, tool_use, max_turns, and stop_sequence—and shows how to handle each in your application. You'll also learn to prevent and recover from empty responses.
Introduction
When you send a request to Claude via the Messages API, the model doesn’t just return text—it also tells you why it stopped generating. This information lives in the stop_reason field of every successful response. Understanding these values is essential for building reliable, production-ready applications that can handle tool calls, incomplete responses, and edge cases gracefully.
In this guide, you’ll learn:
- What each
stop_reasonvalue means - How to handle
end_turn,tool_use,max_tokens, andstop_sequencein your code - How to prevent and recover from empty responses
- Best practices for chaining multiple turns
The stop_reason Field
The stop_reason field is part of every successful Messages API response. Unlike HTTP errors (which indicate a failure to process your request), 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
end_turn
The most common stop reason. It means Claude finished its response naturally—it decided the assistant’s turn was complete. This is what you’ll see for most simple Q&A or conversational flows.
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 decided to call one or more tools. This is common in agentic workflows where Claude needs to fetch data, run calculations, or interact with external systems.
How to handle it:if response.stop_reason == "tool_use":
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 and get result
result = execute_tool(tool_name, tool_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": tool_use_id,
"content": str(result)
}
]
})
# Send the follow-up request
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
max_tokens
Claude hit the max_tokens limit you set. The response is truncated—Claude had more to say but ran out of room.
if response.stop_reason == "max_tokens":
# The response is incomplete. Append Claude's partial response
# and ask it to continue.
messages.append({
"role": "assistant",
"content": response.content
})
messages.append({
"role": "user",
"content": "Please continue from where you left off."
})
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 your request (e.g., "`" or "END"). This is useful for structured outputs like code blocks or JSON.
if response.stop_reason == "stop_sequence":
# The response was cut off at the stop sequence.
# You can extract the content before the sequence.
full_text = response.content[0].text
print(f"Response stopped at custom sequence: {full_text}")
Handling Empty Responses with end_turn
Sometimes Claude returns an empty response (exactly 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"}
# No additional text block
]}
]
Recovering 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-opus-4-7",
max_tokens=1024,
messages=messages
)
# Check if response is empty
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-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
Building a Robust Response Handler
Here’s a complete example that handles all stop reasons in a single loop:
from anthropic import Anthropic
client = Anthropic()
messages = [{"role": "user", "content": "What is 15% of 200? Use the calculator tool."}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn":
print("Final response:", response.content[0].text)
break
elif response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute tool and append result
result = execute_tool(block.name, block.input)
messages.append({
"role": "assistant",
"content": response.content
})
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 from where you left off."
})
elif response.stop_reason == "stop_sequence":
print("Response stopped at custom sequence:", response.content[0].text)
break
Best Practices
- Always check
stop_reason– Don’t assume a response is complete just because you got a 200 status. - Handle
tool_usein a loop – Claude may call multiple tools in sequence or in parallel. - Set
max_tokensappropriately – For long-form content, use a higher limit or implement continuation logic. - Avoid extra text after
tool_result– This prevents empty responses. - Use continuation prompts for truncated responses – A simple "Please continue" works well.
Key Takeaways
- The
stop_reasonfield tells you why Claude stopped:end_turn(natural finish),tool_use(tool call requested),max_tokens(truncated), orstop_sequence(custom stop). - For
tool_use, append the tool result and continue the conversation—never add extra text after the result. - For
max_tokens, append Claude’s partial response and ask it to continue. - Empty responses with
end_turncan be prevented by sending onlytool_resultblocks (no extra text) and recovered with a continuation prompt. - Build a loop that checks
stop_reasonafter every API call to handle multi-turn tool use and truncation gracefully.