Mastering Claude's Stop Reasons: Build Robust API Applications
Learn how to interpret and handle Claude's stop_reason field in the Messages API. This guide covers end_turn, max_tokens, tool_use, and error handling with practical code examples.
This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and shows how to handle each in your API code to build reliable, production-ready applications.
Mastering Claude's Stop Reasons: Build Robust API Applications
When you send a request to Claude via the Messages API, the response includes a stop_reason field that tells you why the model stopped generating. Understanding these values is essential for building applications that handle different scenarios gracefully—whether Claude finished naturally, hit a token limit, or paused to use a tool.
In this guide, you'll learn:
- What each
stop_reasonvalue means - How to handle them in Python and TypeScript
- Common pitfalls (like empty responses) and how to avoid them
- Best practices for production-ready code
What Is stop_reason?
The stop_reason field is part of every successful Messages API response. Unlike error codes (which indicate failures), stop_reason tells you why Claude successfully completed its response generation.
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 stop for four distinct reasons. Let's examine each one.
end_turn
Meaning: Claude finished its response naturally. The model determined it had fully answered the user's request and voluntarily ended its turn.
This is the most common stop reason and usually indicates a successful, complete response.
Handling in Python: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)
Handling in TypeScript:
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);
}
max_tokens
Meaning: Claude stopped because it reached the max_tokens limit you set in your request. The response may be truncated—Claude had more to say but was cut off.
This is critical to handle because the user might receive an incomplete answer.
How to handle it:if response.stop_reason == "max_tokens":
# The response is truncated. Continue the conversation to get more.
messages.append({
"role": "assistant",
"content": response.content
})
messages.append({
"role": "user",
"content": "Please continue."
})
continuation = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
Best practice: Consider increasing max_tokens for complex tasks, or implement automatic continuation logic as shown above.
tool_use
Meaning: Claude stopped because it wants to use a tool (function call). The response will contain one or more tool_use content blocks.
This is the expected behavior when you've provided tools to Claude. Your application must:
- Extract the tool use details from the response
- Execute the tool (e.g., call an API, query a database)
- Return the result as a
tool_resultblock in a new user message
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_id = block.id
# Execute the tool
result = execute_tool(tool_name, tool_input)
# Add tool result to messages
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_id,
"content": str(result)
}]
})
# Continue the conversation
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 API request. This is useful for controlling output format—for example, stopping after a closing XML tag or a specific delimiter.
Example:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["</answer>", "\n\n"],
messages=[{"role": "user", "content": "Give me a short answer.</answer>"}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
print(response.content[0].text)
Handling Empty Responses with end_turn
A common pitfall: Claude sometimes returns an empty response (2–3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use scenarios.
Why It Happens
- Adding text after tool results: If you insert a text block immediately after a
tool_result, Claude may learn to expect that pattern and end its turn prematurely. - Sending Claude's completed response back unchanged: If Claude already decided it's done, sending the same response back won't change its mind.
How to Prevent It
Incorrect approach: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 approach:
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
]}
]
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-sonnet-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"})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Building a Robust Handler
For production applications, you need a handler that covers all scenarios:
def handle_claude_response(client, messages, max_iterations=10):
"""
Handle Claude's response, including tool use and continuation.
"""
iteration = 0
while iteration < max_iterations:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "end_turn":
if not response.content:
# Empty response - prompt to continue
messages.append({"role": "user", "content": "Please continue"})
iteration += 1
continue
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)
}]
})
elif response.stop_reason == "max_tokens":
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
elif response.stop_reason == "stop_sequence":
return response.content[0].text
iteration += 1
raise Exception("Max iterations reached without completion")
Key Takeaways
stop_reasontells you why Claude stopped—not that something went wrong. Use it to decide your next action.end_turnmeans a natural completion;max_tokensmeans truncation;tool_usemeans Claude wants to call a function;stop_sequencemeans a custom delimiter was hit.- Empty responses with
end_turnare common in tool-use flows. Prevent them by sending onlytool_resultblocks (no extra text) and use a "Please continue" prompt if needed. - Always handle
max_tokensby continuing the conversation—otherwise users get incomplete answers. - Build a loop for tool-use scenarios that automatically executes tools and returns results until Claude produces a final answer.