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, including end_turn, tool_use, max_tokens, and stop_sequence scenarios with practical code examples.
This guide explains Claude's stop_reason field values (end_turn, tool_use, max_tokens, stop_sequence) and how to handle each in your application logic, including preventing empty responses and managing tool call flows.
Introduction
When building applications with Claude's Messages API, every successful response includes a stop_reason field that tells you why the model stopped generating. Understanding these values is essential for creating robust, production-ready applications that handle different scenarios appropriately.
Unlike error codes (which indicate something went wrong), stop_reason is part of every successful response. It's your application's signal for what to do next—whether that's displaying the response, continuing a conversation, or processing tool calls.
The stop_reason Field Explained
The stop_reason field appears in every successful Messages API response. 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
}
}
There are four possible values for stop_reason, each requiring a different handling strategy.
Stop Reason Values
end_turn — Natural Completion
This is the most common stop reason. It means Claude finished its response naturally and is ready for the next user turn. 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":
# Display the complete response
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-use scenarios when Claude interprets that the assistant's turn is complete.
- Adding text blocks immediately after tool results in your message history
- Sending Claude's completed response back without adding anything new
# INCORRECT: Adding text immediately after tool_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 add text after tool_result
]}
]
CORRECT: 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
]
If you still get empty responses after fixing the message structure:
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 in a NEW user message
messages.append({
"role": "user",
"content": "Please continue with your response."
})
# Retry with the continuation prompt
return client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
tool_use — Claude Wants to Use a Tool
When Claude decides to use a tool, it stops with stop_reason: "tool_use". Your application must:
- Extract the tool call from the response content
- Execute the tool
- Return the result in a new
usermessage withtool_resultcontent blocks
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Get the current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}],
messages=[{"role": "user", "content": "What's the weather in London?"}]
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute the tool
tool_result = execute_tool(block.name, block.input)
# Append the 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(tool_result)
}
]})
# Continue the conversation
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
max_tokens — Token Limit Reached
Claude stops when it reaches the max_tokens limit you set. The response may be cut off mid-sentence. Your application should:
- Detect this condition
- Optionally continue the conversation with a follow-up prompt
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
async function getCompleteResponse(messages: any[]) {
let response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: messages
});
// If truncated, continue the conversation
while (response.stop_reason === 'max_tokens') {
messages.push({role: 'assistant', content: response.content});
messages.push({role: 'user', content: 'Please continue.'});
response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: messages
});
}
return response;
}
stop_sequence — Custom Stop Sequence Triggered
If you specified stop_sequences in your API request, Claude stops when it encounters one. The stop_sequence field in the response 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 short story"}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# Remove the stop sequence from the content if needed
content = response.content[0].text
if content.endswith(response.stop_sequence):
content = content[:-len(response.stop_sequence)]
Building a Complete Handler
Here's a robust handler that manages all stop reasons:
from anthropic import Anthropic
class ClaudeResponseHandler:
def __init__(self, client: Anthropic, model: str = "claude-sonnet-4-20250514"):
self.client = client
self.model = model
self.messages = []
def process_response(self, response) -> str:
"""Process Claude's response and return the final text."""
if response.stop_reason == "end_turn":
# Natural completion
if response.content:
return response.content[0].text
else:
# Handle empty response
self.messages.append({"role": "user", "content": "Please continue."})
new_response = self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=self.messages
)
return self.process_response(new_response)
elif response.stop_reason == "tool_use":
# Handle tool calls
self.messages.append({"role": "assistant", "content": response.content})
for block in response.content:
if block.type == "tool_use":
result = self.execute_tool(block.name, block.input)
self.messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id, "content": str(result)}
]})
# Continue the conversation
new_response = self.client.messages.create(
model=self.model,
max_tokens=1024,
tools=self.tools,
messages=self.messages
)
return self.process_response(new_response)
elif response.stop_reason == "max_tokens":
# Response was truncated
self.messages.append({"role": "assistant", "content": response.content})
self.messages.append({"role": "user", "content": "Please continue."})
new_response = self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=self.messages
)
return self.process_response(new_response)
elif response.stop_reason == "stop_sequence":
# Custom stop sequence triggered
content = response.content[0].text
if content.endswith(response.stop_sequence):
content = content[:-len(response.stop_sequence)]
return content
return ""
Best Practices
- Always check
stop_reason— Never assume a response is complete without checking the stop reason. - Handle
max_tokensgracefully — Implement continuation logic to ensure users get complete responses. - Structure tool results correctly — Send
tool_resultblocks without additional text to avoid empty responses. - Use continuation prompts for empty responses — Add a new user message with "Please continue" rather than retrying the same input.
- Log stop reasons — Track which stop reasons occur in production to identify patterns and optimize your application.
Key Takeaways
- Four stop reasons exist:
end_turn(natural completion),tool_use(tool call requested),max_tokens(truncated), andstop_sequence(custom stop triggered). - Empty responses with
end_turnoccur when Claude thinks its turn is complete; fix by removing text aftertool_resultblocks or adding a continuation prompt. - Tool use requires multi-step handling: extract the tool call, execute it, and return the result in a new
usermessage. max_tokenstruncation is common with long responses; implement a retry loop with a "Please continue" prompt to get the full response.- Always check
stop_reasonin your application logic to build robust, production-ready Claude integrations.