Mastering Claude API Stop Reasons: Build Smarter, More Reliable Applications
Learn how to interpret and handle Claude API stop_reason values like end_turn, tool_use, and max_tokens. Includes practical code examples, troubleshooting tips for empty responses, and best practices for robust app logic.
This guide explains every Claude API stop_reason value (end_turn, tool_use, max_tokens, stop_sequence) and shows you how to handle them correctly in your code. You'll also learn how to prevent and recover from empty responses.
Introduction
When you send a request to the Claude API, the model generates a response and then stops. But why did it stop? That's where the stop_reason field comes in. This small but mighty piece of data tells you exactly why Claude finished its response — whether it completed naturally, called a tool, hit a token limit, or encountered a custom stop sequence.
Understanding stop_reason is essential for building robust, production-ready applications. Without it, you might miss a tool call, truncate a response, or get stuck in an infinite loop. 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 chaining multi-turn conversations
The stop_reason Field
Every successful response from the Messages API includes a stop_reason field. It's not an error — it's a signal. 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:
| stop_reason | Meaning |
|---|---|
end_turn | Claude finished its response naturally. No further action needed — the model has said everything it wants to say. |
tool_use | Claude wants to call a tool. You must execute the tool and return the result before continuing. |
max_tokens | Claude hit the max_tokens limit you set. The response is truncated. You should continue the conversation to get the rest. |
stop_sequence | Claude encountered a custom stop sequence you defined in the API request. |
Handling Each Stop Reason
1. end_turn — Natural Completion
This is the most common and simplest case. Claude has finished its response and expects no further action.
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":
print(response.content[0].text)
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);
}
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 stops with stop_reason: "tool_use". Your application must:
- Extract the tool call details from the response content.
- Execute the tool.
- Send the tool result back in a new
usermessage.
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=[{"role": "user", "content": "What is 1234 + 5678?"}]
)
if response.stop_reason == "tool_use":
tool_call = response.content[0]
# Execute the tool (in real code, call your function)
result = execute_tool(tool_call.name, tool_call.input)
# Send the result back
messages = [
{"role": "user", "content": "What is 1234 + 5678?"},
{"role": "assistant", "content": [tool_call]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tool_call.id, "content": str(result)}
]}
]
response = client.messages.create(model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages)
3. max_tokens — Truncated Response
If Claude hits the max_tokens limit, the response is cut off. You should continue the conversation to get the remaining content.
if response.stop_reason == "max_tokens":
# Append Claude's partial response and ask to continue
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=1024, messages=messages)
4. stop_sequence — Custom Stop
If you defined a stop_sequences parameter (e.g., ["END"]), Claude stops when it encounters that sequence. The stop_sequence field tells you which one was hit.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
stop_sequences=["END"],
messages=[{"role": "user", "content": "List three colors and then write END."}]
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# The content will include everything before the stop sequence
print(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 scenarios when:
- You add text blocks immediately after
tool_resultblocks. - You send Claude's completed response back without adding anything new.
Why Does This Happen?
Claude learns from the conversation pattern. If you always insert text after tool results, Claude expects that pattern and ends its turn early. Similarly, if you send back a response where Claude already decided it's done, it will remain "done."
How to Prevent Empty Responses
Incorrect — adding text 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 here
]}
]
Correct — send tool results directly:
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 result
]}
]
Recovering from Empty Responses
If you still get empty responses after fixing the above, do not retry with the same messages — Claude has already decided it's done. Instead, 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:
# ✅ Correct: Add a continuation prompt
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-4-20250514",
max_tokens=1024,
messages=messages
)
return response
Building a Robust Stop Reason Handler
Here's a complete pattern you can use in production:
def handle_response(client, messages, tools=None):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=tools
)
if response.stop_reason == "end_turn":
if response.content:
return response.content[0].text
else:
# Empty response — prompt to continue
messages.append({"role": "user", "content": "Please continue"})
return handle_response(client, messages, tools)
elif response.stop_reason == "tool_use":
tool_call = response.content[0]
result = execute_tool(tool_call.name, tool_call.input)
messages.append({"role": "assistant", "content": [tool_call]})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tool_call.id, "content": str(result)}
]})
return handle_response(client, messages, tools)
elif response.stop_reason == "max_tokens":
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "Please continue."})
return handle_response(client, messages, tools)
elif response.stop_reason == "stop_sequence":
return response.content[0].text
Key Takeaways
stop_reasonis your app's compass — it tells you exactly why Claude stopped, so you can respond appropriately.- Handle
tool_useby executing the tool and returning the result in a new user message with atool_resultblock. - For
max_tokens, continue the conversation by appending Claude's partial response and asking it to continue. - Prevent empty responses by never adding text blocks immediately after
tool_resultblocks, and always sending fresh context when retrying. - Build a recursive handler that checks
stop_reasonand loops until you get a completeend_turnresponse with content.