Mastering Claude API Stop Reasons: A Practical Guide to Handling end_turn, tool_use, and max_tokens
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 development.
This guide explains Claude API stop_reason values (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each in your code. You'll learn to detect empty responses, manage tool call loops, and build robust conversational flows.
Introduction
When you call the Claude API, every successful response includes a stop_reason field. This small but mighty piece of data tells you why Claude stopped generating—whether it finished naturally, wants to use a tool, hit a token limit, or encountered a stop sequence. Misunderstanding these signals can lead to broken conversations, empty responses, or infinite loops.
In this guide, you'll learn exactly what each stop_reason means, how to handle it in Python and TypeScript, and how to avoid common pitfalls like empty responses after tool calls.
The stop_reason Field
The stop_reason field appears in every successful Messages API response. Unlike error codes (which indicate failures), stop_reason tells you why Claude successfully stopped generating. Here's a typical response:
{
"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
What it means: Claude finished its response naturally. The model decided it had completed its turn in the conversation. How to handle it: This is the simplest case—just process the response content.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)
#### 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.
- 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)
# 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 after fixing the above:
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:
# CORRECT: 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
tool_use
What it means: Claude wants to call one or more tools. The response content will containtool_use blocks with the tool name and input parameters.
How to handle it: You must execute the tool, return the result as a tool_result block, and continue the conversation.
def handle_tool_use(response, messages):
# Extract tool calls from 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 assistant response and 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
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
max_tokens
What it means: Claude stopped because it reached themax_tokens limit you set. The response may be cut off mid-sentence.
How to handle it: You can prompt Claude to continue from where it left off.
def handle_max_tokens(response, messages):
if response.stop_reason == "max_tokens":
# Add the partial response to messages
messages.append({"role": "assistant", "content": response.content})
# Ask Claude to continue
messages.append({"role": "user", "content": "Please continue from where you left off."})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048, # Consider increasing the limit
messages=messages
)
return response
stop_sequence
What it means: Claude encountered a custom stop sequence you defined in your API request. This is useful for structured outputs or when you want Claude to stop at a specific delimiter. How to handle it: Thestop_sequence field in the response will contain the actual sequence that was matched. You can use this to parse structured content.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\n---END---"],
messages=[{"role": "user", "content": "Write a short poem and end with ---END---"}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# The content will be everything before the stop sequence
print(response.content[0].text)
Building a Complete Handler
For production applications, you should handle all stop reasons in a single loop:
def process_conversation(client, messages, max_iterations=10):
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":
# Conversation complete
return response.content[0].text if response.content else ""
elif response.stop_reason == "tool_use":
# Handle tool calls
messages.append({"role": "assistant", "content": response.content})
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":
# Continue generation
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
elif response.stop_reason == "stop_sequence":
# Custom stop sequence reached
return response.content[0].text
raise Exception("Max iterations reached without completion")
Best Practices
- Always check stop_reason before processing content. Don't assume
end_turn. - Handle empty responses gracefully. Implement the continuation prompt pattern for empty
end_turnresponses. - Set appropriate max_tokens. If you frequently see
max_tokensstops, increase the limit or implement continuation logic. - Use stop_sequences for structured output. Define clear delimiters when you need Claude to output in a specific format.
- Log stop_reason for debugging. Track which stop reasons occur in production to fine-tune your prompts and parameters.
Key Takeaways
- Four stop reasons exist:
end_turn(natural completion),tool_use(wants to call a tool),max_tokens(hit token limit), andstop_sequence(custom delimiter reached). - Empty responses with
end_turnare usually caused by adding text after tool results—send onlytool_resultblocks in user messages. - Tool use requires a loop: When you get
tool_use, execute the tool, return the result, and continue the conversation. max_tokensdoesn't mean failure: Simply prompt Claude to continue with a new user message.- Always build a handler for all stop reasons to create robust, production-ready applications.