Mastering Claude API Stop Reasons: A Practical Guide to Handling Response Endings
Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes code examples and fixes for empty responses.
This guide explains Claude's stop_reason field (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each in your application. You'll learn to detect empty responses, continue conversations properly, and build robust multi-turn interactions.
Introduction
When you send a request to the Claude API, the response includes a stop_reason field that tells you why the model stopped generating. This isn't an error—it's a signal. Understanding these signals is essential for building reliable applications that handle tool calls, truncated responses, and natural conversation endings correctly.
In this guide, you'll learn:
- What each
stop_reasonvalue means - How to handle
end_turn,tool_use,max_tokens, andstop_sequence - How to prevent and recover from empty responses
- Best practices for multi-turn conversations with tools
The stop_reason Field
Every successful response from the Messages API includes a stop_reason field. 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
Most common. Claude finished its response naturally—it decided the assistant's turn was complete. This is the default for simple Q&A.
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
Claude wants to call a tool. The response content will contain one or more tool_use blocks. You must execute the tool and send the result back in a new user message with tool_result content.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=[{
"name": "get_weather",
"description": "Get current weather",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"}
}
}
}]
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# Execute the tool and send result back
result = execute_tool(tool_name, tool_input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
max_tokens
Claude hit the max_tokens limit you set. The response is truncated. To continue, append a new user message with a continuation prompt like "Please continue."
if response.stop_reason == "max_tokens":
# The response was cut off
partial_text = response.content[0].text
# Add a continuation prompt
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 your request. This is useful for structured outputs where you want to stop generation at a specific marker.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "List three colors"}],
stop_sequences=["\n"] # Stop at first newline
)
if response.stop_reason == "stop_sequence":
print("Stopped at custom sequence:", response.stop_sequence)
Handling Empty Responses with end_turn
Sometimes Claude returns an empty response (2–3 tokens, no content) with stop_reason: "end_turn". This typically happens after tool results when Claude decides the assistant turn is complete.
Common Causes
- Adding text after tool results – Claude learns to expect the user to 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 stays done.
How to Prevent Empty Responses
Incorrect: Adding text immediately aftertool_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
]
}
]
Recovering from Empty Responses
If you still get empty responses after fixing the above, use a continuation prompt in a new user message:
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:
# ✅ Add a continuation prompt
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 handler that processes all stop reasons correctly:
def handle_claude_response(client, messages, tools=None):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=tools
)
if response.stop_reason == "end_turn":
if not response.content:
# Empty response – prompt to continue
messages.append({"role": "user", "content": "Please continue"})
return handle_claude_response(client, messages, tools)
return response.content[0].text
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)
}]
})
# Continue the conversation
return handle_claude_response(client, messages, tools)
elif response.stop_reason == "max_tokens":
messages.append({"role": "user", "content": "Please continue"})
return handle_claude_response(client, messages, tools)
elif response.stop_reason == "stop_sequence":
return response.content[0].text
Best Practices
- Always check
stop_reason– Don't assume the response is complete. Handle each case explicitly. - Never add text after
tool_result– Send only the tool result in theusermessage to avoid empty responses. - Use continuation prompts for truncation – When
max_tokensis hit, append a new user message with "Please continue" rather than retrying the same prompt. - Log stop reasons – In production, log
stop_reasonto debug unexpected behavior and monitor usage patterns. - Set appropriate
max_tokens– For long-form content, setmax_tokenshigh enough to avoid frequent truncation.
Key Takeaways
end_turnmeans Claude finished naturally;tool_usemeans it wants to call a tool;max_tokensmeans the response was truncated;stop_sequencemeans a custom stop marker was hit.- Empty responses with
end_turnare usually caused by adding text aftertool_resultblocks—send only the tool result. - Recover from empty responses by appending a new user message with "Please continue"—don't retry the same messages.
- Build a recursive handler that processes tool calls and truncation automatically for robust multi-turn conversations.
- Log and monitor stop reasons in production to catch issues early and optimize your application's behavior.