Mastering Claude's Stop Reasons: Build Smarter API Applications
Learn how to interpret and handle Claude's stop_reason field in the Messages API. Includes code examples for end_turn, max_tokens, tool_use, and error handling strategies.
This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and how to handle each in your API application to build robust, production-ready integrations.
When you call the Claude Messages API, every successful response includes a stop_reason field. This small but powerful piece of data tells you why Claude stopped generating—whether it finished naturally, hit a token limit, requested a tool call, or matched a custom stop sequence. Understanding these reasons is essential for building applications that respond intelligently to different scenarios.
In this guide, you'll learn what each stop reason means, how to handle them in Python and TypeScript, and how to avoid common pitfalls like empty responses.
What Is stop_reason?
The stop_reason field appears in every successful API response. It is not an error—it's a signal that Claude completed its response generation and is telling you why it stopped.
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 Four Stop Reasons
Claude can return one of four stop_reason values:
| Value | Meaning |
|---|---|
end_turn | Claude finished its response naturally |
max_tokens | Claude hit the max_tokens limit you set |
tool_use | Claude wants to call a tool (function) |
stop_sequence | Claude encountered a custom stop sequence |
end_turn: Natural Completion
This is the most common stop reason. Claude decided it had fully answered the user's request and stopped on its own.
Python example:from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "What is the capital of France?"}]
)
if response.stop_reason == "end_turn":
print("Claude finished naturally:")
print(response.content[0].text)
TypeScript example:
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
async function main() {
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'What is the capital of France?' }]
});
if (response.stop_reason === 'end_turn') {
console.log('Claude finished naturally:');
console.log(response.content[0].text);
}
}
#### 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 in tool-use workflows when Claude decides the assistant's turn is complete.
- Adding text blocks immediately after
tool_resultblocks - Sending Claude's completed response back without adding anything new
# INCORRECT: Adding text after tool_result
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 do this
]
}
]
CORRECT: Send tool results directly
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"}
] # No extra text
}
]
If you still get empty responses, add 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 with your response."
})
# Retry
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
max_tokens: Token Limit Reached
Claude stopped because it hit the max_tokens limit you set. This is common for long responses.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=200, # Low limit for demonstration
messages=[{"role": "user", "content": "Write a detailed essay on AI safety."}]
)
if response.stop_reason == "max_tokens":
print("Response was truncated. Consider increasing max_tokens.")
# You can continue the conversation by sending Claude's response back
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=2000,
messages=messages
)
Best practice: Set max_tokens generously for open-ended tasks, or implement a loop that continues the conversation when max_tokens is hit.
tool_use: Claude Wants to Call a Tool
When you provide tools (functions), Claude may decide to call one. The response will contain a tool_use content block.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}],
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
print(f"Claude wants to call {tool_name} with {tool_input}")
# Execute the tool and send result back
result = execute_tool(tool_name, tool_input)
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
# Continue the conversation
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
stop_sequence: Custom Stop Sequence Matched
If you set a stop_sequences parameter in your API call, Claude will stop when it encounters that sequence.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["\n\nHuman:"], # Stop before a new human turn
messages=[{"role": "user", "content": "Tell me a story about a robot."}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
print(response.content[0].text)
Building a Robust Handler
Combine all stop reasons into a single handler:
def handle_response(response, messages, client):
if response.stop_reason == "end_turn":
if not response.content:
# Handle empty response
messages.append({"role": "user", "content": "Please continue."})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response # Complete
elif response.stop_reason == "max_tokens":
# Continue the conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
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": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
return client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
elif response.stop_reason == "stop_sequence":
# Handle custom stop
print(f"Custom stop at: {response.stop_sequence}")
return response
else:
raise ValueError(f"Unknown stop_reason: {response.stop_reason}")
Key Takeaways
stop_reasonis always present in successful API responses and tells you why Claude stopped generating.end_turnmeans natural completion—but watch for empty responses in tool workflows.max_tokensmeans your token limit was hit; implement a continuation loop for long responses.tool_usemeans Claude wants to call a function; execute the tool and send results back.stop_sequencemeans a custom stop pattern was matched; handle it based on your application logic.- Always check
stop_reasonin your code to build robust, production-ready Claude integrations.