Mastering Claude API Stop Reasons: Build Robust Applications with end_turn, tool_use & max_tokens
Learn how to handle Claude API stop_reason values (end_turn, tool_use, max_tokens) to build reliable applications. Includes code examples and troubleshooting tips.
This guide explains Claude's stop_reason field—end_turn, tool_use, max_tokens, and stop_sequence—and shows you how to handle each one in your code to prevent empty responses, continue tool loops, and manage token limits.
Introduction
Every time you call the Claude Messages API, the response includes a stop_reason field. This tiny piece of data tells you why Claude stopped generating—whether it finished naturally, requested a tool call, hit a token limit, or encountered a stop sequence. Ignoring stop_reason is like driving without looking at your dashboard: you might get where you're going, but you'll miss critical signals along the way.
In this guide, you'll learn what each stop_reason value means, how to handle them in Python and TypeScript, and how to avoid common pitfalls like empty responses and broken tool loops.
Understanding the stop_reason Field
The stop_reason field appears in every successful Messages API response. It's not an error—it's a signal about completion. 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
}
}
There are four possible values for stop_reason:
| Value | Meaning |
|---|---|
end_turn | Claude finished its response naturally |
tool_use | Claude wants to call a tool (function) |
max_tokens | Claude hit the max_tokens limit you set |
stop_sequence | Claude encountered a custom stop sequence |
1. end_turn: The Natural Finish
end_turn is the most common stop reason. It means Claude believes it has completed its response and is ready for the next user input. In most cases, you can simply display the response to the user.
Python Example
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)
TypeScript Example
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }]
});
if (response.stop_reason === 'end_turn') {
console.log(response.content[0].text);
}
The Empty Response Problem
Sometimes Claude returns an empty response (2–3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use scenarios when:
- You add text blocks immediately after
tool_resultblocks - You send 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"} # ✅ Just the result
]}
]
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-20250514",
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."
})
return client.messages.create(
model="claude-opus-4-20250514",
max_tokens=1024,
messages=messages
)
return response
2. tool_use: Claude Wants to Call a Tool
When Claude decides it needs to use a tool (e.g., a calculator, database query, or API call), it returns stop_reason: "tool_use" along with one or more tool_use content blocks. Your application must:
- Extract the tool call details
- Execute the tool
- Return the result as a
tool_resultblock - Continue the conversation
Python Tool Loop
def run_tool_loop(client, messages):
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "calculator",
"description": "Perform arithmetic operations",
"input_schema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["operation", "a", "b"]
}
}],
messages=messages
)
if response.stop_reason == "end_turn":
return response.content[0].text
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute the tool
result = execute_tool(block.name, block.input)
# Append assistant's response and tool result
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
TypeScript Tool Loop
async function runToolLoop(client: Anthropic, messages: any[]) {
while (true) {
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [/ your tool definitions /],
messages
});
if (response.stop_reason === 'end_turn') {
return response.content[0].text;
}
if (response.stop_reason === 'tool_use') {
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await executeTool(block.name, block.input);
messages.push({ role: 'assistant', content: response.content });
messages.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: block.id,
content: String(result)
}]
});
}
}
}
}
}
3. max_tokens: Hit the Token Limit
When Claude's response is cut off because it reached the max_tokens limit, you get stop_reason: "max_tokens". This means the response is incomplete. To continue, send a follow-up message asking Claude to finish.
def handle_max_tokens(client, messages, response):
if response.stop_reason == "max_tokens":
# Append the partial response
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=1024,
messages=messages
)
return response
Pro tip: Increase max_tokens if you frequently hit this limit, or implement automatic continuation in a loop.
4. stop_sequence: Custom Stop Sequences
If you defined custom stop_sequences in your API request, Claude will stop when it encounters one. The stop_sequence field will contain the matched sequence.
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":
print(f"Stopped at sequence: {response.stop_sequence}")
# The response is complete up to the stop sequence
Best Practices Summary
- Always check
stop_reasonbefore processing the response content - For
tool_use: Implement a loop that executes tools and returns results - For
max_tokens: Continue the conversation with a prompt to finish - For
end_turn: Display the response, but watch for empty responses in tool contexts - Avoid adding text after
tool_resultblocks to prevent empty responses
Key Takeaways
stop_reasontells you why Claude stopped, not that something went wrong—it's a completion signal, not an errorend_turnmeans Claude finished naturally; handle empty responses by sending a continuation prompt in a new user messagetool_userequires a loop: extract the tool call, execute it, return the result, and continue the conversationmax_tokensmeans the response is incomplete; send a follow-up prompt to let Claude finishstop_sequenceis triggered by custom sequences you define; the matched sequence is returned in the response