Mastering Claude API Stop Reasons: A Practical Guide to Handling Response Terminations
Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes code examples and troubleshooting tips for robust applications.
This guide explains Claude API stop_reason values (end_turn, tool_use, max_tokens, stop_sequence) and how to handle them in your code. You'll learn to detect empty responses, manage tool call loops, and build robust applications that respond correctly to each termination type.
Introduction
When you send a request to the Claude Messages 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 applications that handle different response types appropriately, especially when working with tools, streaming, or multi-turn conversations.
In this guide, you'll learn:
- What each
stop_reasonvalue means - How to handle them in Python and TypeScript
- How to prevent and fix empty responses
- Best practices for tool-using agents
The stop_reason Field
Every successful Messages API response 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
}
}
The stop_reason field can have one of four values: end_turn, tool_use, max_tokens, or stop_sequence. Let's explore each.
Stop Reason Values
end_turn
Meaning: Claude finished its response naturally. The model decided it had completed its turn and didn't need to continue.
When it occurs: This is the most common stop reason. It happens when Claude has fully answered a question, completed a task, or simply said "I'm done."
How to handle it: Process the response content as final. No further action is needed.
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
Meaning: Claude wants to call a tool. The response will contain one or more tool_use content blocks.
When it occurs: When you've provided tools in your request and Claude decides it needs to use one (or more) to complete the task.
How to handle it: Extract the tool calls, execute them, and send the results back in a new user message with tool_result blocks.
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
result = execute_tool(tool_name, tool_input)
# Append tool result to messages
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": str(result)
}]
})
# Continue the conversation
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
max_tokens
Meaning: Claude stopped because it reached the max_tokens limit you set.
When it occurs: The response was cut off mid-generation. The content may be incomplete.
How to handle it: You have two options:
- Increase
max_tokensif you need longer responses. - Send the partial response back to let Claude continue from where it left off.
if response.stop_reason == "max_tokens":
# Option 1: Increase max_tokens and retry
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096, # Increased limit
messages=messages
)
# Option 2: Continue from where it stopped
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
stop_sequence
Meaning: Claude stopped because it encountered a custom stop sequence you defined in your request.
When it occurs: You've set stop_sequences in your API call, and Claude generated one of them.
How to handle it: The content up to (but not including) the stop sequence is valid. Process it as a complete response.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\nHuman:"],
messages=[{"role": "user", "content": "Tell me a story"}]
)
if response.stop_reason == "stop_sequence":
# The response stopped because it hit the stop sequence
print(f"Stopped at sequence: {response.stop_sequence}")
print(response.content[0].text)
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-using scenarios.
Common Causes
- Adding text blocks 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 remains done.
How to Prevent Empty Responses
Incorrect approach: 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 do this!
]}
]
Correct approach: 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
]
Handling Empty Responses When They Occur
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
)
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 with your response."
})
return 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:
from anthropic import Anthropic
client = Anthropic()
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
def handle_response(response, messages):
if response.stop_reason == "end_turn":
# Natural completion - process the response
print("Final response:", response.content[0].text)
return response
elif response.stop_reason == "tool_use":
# Claude wants to use tools
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
new_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return handle_response(new_response, messages)
elif response.stop_reason == "max_tokens":
# Response was cut off - continue
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
new_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return handle_response(new_response, messages)
elif response.stop_reason == "stop_sequence":
# Custom stop sequence encountered
print("Response stopped by custom sequence:", response.stop_sequence)
return response
Initial call
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
final_response = handle_response(response, messages)
Best Practices
- Always check
stop_reason: Don't assume a response is complete just because you got a 200 status code. - Handle
tool_usein a loop: Tool-using agents may need multiple rounds of tool calls to complete a task. - Set appropriate
max_tokens: If you expect long responses, setmax_tokenshigh enough to avoid truncation. - Log
stop_reason: For debugging and monitoring, always log the stop reason along with the response. - Test with empty responses: Build your application to gracefully handle empty
end_turnresponses, especially when using tools.
Key Takeaways
- Four stop reasons:
end_turn(natural completion),tool_use(wants to call a tool),max_tokens(hit token limit), andstop_sequence(hit custom stop sequence). - Handle
tool_useby executing tools and sending results back in a new user message withtool_resultblocks. - Prevent empty responses by not adding text blocks after
tool_resultblocks in your messages array. - For
max_tokens, either increase the limit or send the partial response back with a continuation prompt. - Build a recursive handler that processes all stop reasons correctly, especially for tool-using agents that may need multiple rounds.