Mastering Claude’s Stop Reasons: A Practical Guide for API Developers
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 every Claude API stop_reason value, how to handle them in code, and how to prevent or recover from empty responses. You’ll learn practical patterns for end_turn, tool_use, max_tokens, and stop_sequence.
Introduction
When you call the Claude Messages API, every successful response includes a stop_reason field. This small but critical piece of data tells you why Claude stopped generating—whether it finished naturally, needs to call a tool, hit a token limit, or encountered a stop sequence. Misunderstanding or ignoring stop_reason can lead to broken workflows, incomplete answers, or silent failures.
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 robust multi-turn conversations
The stop_reason Field
The stop_reason field appears in every successful response from the Messages API. It is not an error—it’s a signal that Claude finished its response generation for a specific reason.
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
Meaning: Claude finished its response naturally. This is the most common stop reason and usually indicates a complete answer. How to handle: Process the response content as final.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":
print(response.content[0].text)
tool_use
Meaning: Claude wants to call a tool (function) you’ve defined. The response will contain one or moretool_use content blocks.
How to handle: Extract the tool name and input, execute the tool, then send the result back 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":
tool_name = block.name
tool_input = block.input
tool_id = block.id
# Execute your tool logic here
result = my_tool_function(tool_name, tool_input)
# Append tool_result to conversation
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_id,
"content": str(result)
}]
})
max_tokens
Meaning: Claude stopped because it reached themax_tokens limit you set. The response may be cut off mid-sentence.
How to handle: This is not an error—it’s a signal that you should continue the conversation. Append a new user message like “Please continue” to let Claude finish.
if response.stop_reason == "max_tokens":
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
Meaning: Claude encountered a custom stop sequence you defined in your API request (e.g.,"\n\nHuman:" or "<END>").
How to handle: The response is complete up to the stop sequence. 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 content normally
Handling Empty Responses with end_turn
A common pitfall: Claude returns an empty response (2–3 tokens, no content) with stop_reason: "end_turn". This typically happens in tool-use workflows.
Why It Happens
- Adding text after tool results: If you insert a text block immediately after a
tool_result, Claude learns to expect that pattern and may end its turn early. - Resending Claude’s completed response: If you send back Claude’s own finished response without adding anything new, Claude sees it’s done and stays done.
How to Prevent Empty Responses
Incorrect pattern: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 add text here
]}
]
Correct pattern:
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 tool_result
]}
]
Recovery Strategy
If you still get empty responses, do not retry with the same empty response—Claude will remain done. Instead, add a new user message with 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:
# ✅ Add 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
- Always check stop_reason – Don’t assume
end_turnmeans success. Validate the content length and type. - Handle tool_use in a loop – Claude may call multiple tools in sequence. Keep the conversation going until you get
end_turn. - Set reasonable max_tokens – If you expect long responses, set
max_tokenshigh enough to avoid truncation. - Use stop_sequences for structured output – Define custom stop sequences like
"\n\nNext:"to break long outputs into chunks. - Log stop_reason and stop_sequence – For debugging and monitoring, always log these fields alongside token usage.
Full Example: Multi-Turn Tool Loop
from anthropic import Anthropic
client = Anthropic()
messages = [{"role": "user", "content": "What's the weather in Tokyo and London?"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute tool and append result
result = get_weather(block.input["location"])
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id, "content": result}
]})
elif response.stop_reason == "end_turn":
print(response.content[0].text)
break
elif response.stop_reason == "max_tokens":
messages.append({"role": "user", "content": "Please continue."})
else:
# stop_sequence or unexpected
break
Key Takeaways
end_turnmeans Claude finished naturally;tool_usemeans it wants to call a tool;max_tokensmeans it hit your limit;stop_sequencemeans it hit a custom stop.- Never add text blocks after
tool_result– this is the most common cause of empty responses. - To recover from empty responses, add a new user message with “Please continue” rather than retrying the same input.
- Always loop on
tool_useuntil you getend_turnto handle multi-tool conversations. - Log
stop_reasonandstop_sequencefor debugging and monitoring response behavior.