Mastering Claude’s Stop Reasons: A Practical Guide to Handling API Responses
Learn how to interpret and handle Claude's stop_reason field in the Messages API. Covers end_turn, tool_use, max_tokens, and empty response prevention with code examples.
This guide explains Claude’s stop_reason field—end_turn, tool_use, max_turns, and stop_sequence—and shows how to handle each in your code. You’ll also learn to prevent and recover from empty responses when using tools.
Introduction
When you call the Claude Messages API, every successful response includes a stop_reason field. This small but critical piece of data tells you why Claude stopped generating text. Understanding these values is essential for building reliable, production-ready applications—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 recover from empty responses
- Best practices for robust API integration
The stop_reason Field
Every successful response from the Messages API contains a stop_reason field. It is not an error—it’s a signal that Claude completed its response generation for a specific reason.
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
The most common stop reason. Claude finished its response naturally—it decided the conversation turn was complete. This usually means you can display the final 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();
async function main() {
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);
}
}
tool_use
Claude stopped because it wants to call a tool. The response content will include a tool_use block with the tool name and input. Your application must execute the tool and return the result in a new user message with role: "user" and content of type tool_result.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=[{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}]
)
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 tool and get result
result = execute_tool(tool_name, tool_input)
# Append tool result to messages
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,
tools=tools
)
max_tokens
Claude reached the token limit you set in max_tokens. The response is incomplete. You should either:
- Increase
max_tokensfor future requests - Send the response back to Claude with a prompt like "Please continue" to get more output
if response.stop_reason == "max_tokens":
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=2048, # Increase limit
messages=messages
)
stop_sequence
Claude encountered a custom stop sequence you defined in the request. This is useful for structured outputs—for example, stopping generation after a closing XML tag.
Example:response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["</answer>"],
messages=[{"role": "user", "content": "Write a short answer in XML: <answer>...</answer>"}]
)
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 in tool-use scenarios when Claude decides the assistant turn is complete.
Common Causes
- Adding text immediately 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 pattern: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 pattern:
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
]}
]
Recovery from Empty Responses
If you still get empty responses after fixing the message structure, use a continuation prompt:
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"})
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
Best Practices
- Always check
stop_reason– Don’t assumeend_turn. Handle all possible values. - Use a loop for tool_use – Claude may call multiple tools in sequence. Keep calling until you get
end_turn. - Increase
max_tokensfor long responses – If you frequently hitmax_tokens, raise the limit. - Avoid extra text after tool results – Send only the
tool_resultblock to prevent empty responses. - Log stop reasons – For debugging and monitoring, log the
stop_reasonandstop_sequencefor each response.
Conclusion
Understanding stop_reason is fundamental to building reliable Claude-powered applications. By handling each reason appropriately—continuing after tool_use, recovering from max_tokens, and preventing empty responses—you can create smooth, robust conversational experiences.
Key Takeaways
stop_reasontells you why Claude stopped – Always check it to decide the next action.end_turnmeans the response is complete – Display it to the user.tool_userequires you to execute a tool and return the result – Use a loop to handle multiple tool calls.max_tokensmeans the response was cut off – Increase the limit or ask Claude to continue.- Prevent empty responses by sending only
tool_resultblocks – Never add extra text after tool results.