When Claude uses tools, the SDK enforces strict message pairing: every tool_use block in an assistant message must be followed by a matching tool_result block in a user message. Get the pairing wrong and Anthropic returns Error 400: tool_result blocks must be a response to tool_use in the previous turn. Here is the exact pattern that works in 2026 production tool routing.
Why the pairing rule exists
Claude tracks tool calls across a conversation. Assistant messages contain tool_use blocks describing calls. Your app runs the actual tools and sends results back as tool_result blocks in the next user message. Skipping this pairing means Claude has no way to know how its calls resolved. The SDK enforces it strictly to prevent silently broken agents.
The correct pattern
import anthropic
client = anthropic.Anthropic()
TOOLS = [{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
def run_tool(name: str, args: dict) -> str:
if name == "get_weather":
return f"Sunny 28C in {args['city']}"
raise ValueError(f"Unknown tool: {name}")
def agent_turn(messages: list) -> list:
"""Run one agent turn. Returns updated messages."""
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
tools=TOOLS,
messages=messages,
)
# Add assistant response to messages
messages.append({"role": "assistant", "content": response.content})
# If Claude called tools, run them and add results
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
# CRITICAL: tool_results go in a USER message, not assistant
messages.append({"role": "user", "content": tool_results})
return messages
# Multi-turn loop
messages = [{"role": "user", "content": "What is the weather in Manila?"}]
while True:
messages = agent_turn(messages)
last = messages[-1]
if last["role"] == "assistant":
# Check if final answer (no tool_use)
if not any(b.type == "tool_use" for b in last["content"] if hasattr(b, "type")):
break
if len(messages) > 20:
break # safety capCommon pairing bugs and fixes
- Bug: putting tool_result in an assistant message. Fix: tool_result blocks MUST go in a user message. The user message can contain nothing else if the conversation continues with the assistant’s next call.
- Bug: skipping tool_result for a tool_use. Every tool_use needs a matching tool_result. If a tool errored, still send a tool_result with the error text and
is_error: True. - Bug: mismatched tool_use_id. Every tool_result must reference the exact tool_use_id from the tool_use block. Copy the ID verbatim.
- Bug: inserting other user text between tool_use and tool_result. Fix: tool_result must be in the IMMEDIATELY next user message. No user text turn in between.
Handling tool errors correctly
def run_tool_safe(name: str, args: dict) -> dict:
"""Return a tool_result dict with is_error flag if failed."""
try:
result = run_tool(name, args)
return {"type": "tool_result", "content": result}
except Exception as e:
return {
"type": "tool_result",
"content": f"Tool failed: {e}",
"is_error": True,
}
# In the agent loop
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool_safe(block.name, block.input)
result["tool_use_id"] = block.id
tool_results.append(result)
messages.append({"role": "user", "content": tool_results})Debugging checklist
- Log every message’s role + block types before sending. Confirm the pattern: user → assistant(tool_use) → user(tool_result) → assistant(next_use_or_answer).
- If Claude claims to have called a tool it did not, check for missing tool_result on a previous tool_use. Claude will retry after seeing the gap.
- For parallel tool calls in one assistant turn, send ALL tool_results in a single user message before continuing.
- If you cache or store messages, preserve the exact tool_use_id string. Reformatting them (uppercasing, whitespace) breaks pairing.
Common mistakes when routing tools
- Adding a user narration between tool_use and tool_result. Breaks pairing. If you need to log or narrate, do it outside the message array.
- Serializing tool_result content as JSON when Claude expects a string or block list. Send plain strings for simple cases. Use a list of content blocks for multi-modal returns.
- Forgetting is_error=True on tool failure. Without it, Claude treats the failure text as successful output.
- Running tools sequentially when Claude called them in parallel. Not a bug per se, but slow. Use asyncio.gather for parallel tool execution.
Debugging checklist for the Anthropic SDK error
Before diving into fixes, run through this diagnostic checklist. Nine times out of ten the answer surfaces here.
- Read the full traceback, not just the error message. The stack trace shows exactly which line and which call chain triggered the error. The last line names the immediate cause; earlier lines show how you got there.
- Add print or debug statements just before the failing line. Print the variable, its type, and its value. Nine out of ten error surprises come from the value being different from what you assumed.
- Check Python version compatibility. Errors sometimes result from APIs that changed between versions. Run your interpreter version check and compare against the library documentation for that version.
- Isolate the failing call in a minimal reproducer. Copy the failing line into a small standalone script with hardcoded inputs. If it fails there too, the bug is in your code. If not, something in your surrounding context is contributing.
- Search the exact error message. Include the class name and the specific text in your search. Chances are someone else hit the same issue and the fix is documented on Stack Overflow or the library’s GitHub issues.
Common causes for the Anthropic SDK error
Most instances of this error trace back to one of these root causes:
- Uninitialized or missing input. A variable was not populated before use, or the input source (file, API response, database row) did not contain the expected key or value.
- Type mismatch. The code expected a specific type (dict, list, string) but received something different. Python’s dynamic typing means this often surfaces at runtime, not at compile time.
- Version drift. The library API changed and your code assumes the old signature. Check the library’s changelog for breaking changes since the version you last used.
- Race condition or ordering issue. Async or concurrent code sometimes tries to access data before it is ready. Add awaits, locks, or explicit ordering to fix.
- Copy-paste from stale tutorial. Older tutorials may use APIs that no longer exist. Always check the official docs for the current version.
Testing and prevention
Preventing this class of error from recurring is more valuable than fixing it once. Build these habits into your workflow:
- Write tests that trigger the error path. If your test suite hits the error scenario, catch and assert it. A well-written test prevents the same bug from returning.
- Validate inputs at API boundaries. When data enters your code from external sources (HTTP requests, file uploads, database queries), validate structure and types immediately.
- Use type hints and static analysis. Tools like mypy for Python or TypeScript for JavaScript catch many type mismatches before you run the code.
- Log important state. Structured logging with context helps you debug production issues faster. Include enough context to reconstruct what happened.
- Read the library changelog. Before upgrading a dependency, skim the changelog for breaking changes. Two minutes of reading saves an hour of debugging.
When to ask for help
Some errors are worth solving yourself for the learning. Others are worth asking about early. Ask for help when: the error blocks a customer-facing feature, you have spent an hour without progress, the error involves security or data integrity, or you are unsure whether your fix will introduce new bugs. Post to Stack Overflow with a minimal reproducer, or ask a senior developer on your team. Time boxes are your friend.
Official documentation
Frequently Asked Questions
Can tool_result contain a list of content blocks?
Yes. For multi-modal returns (text + image), send content as a list of blocks. Single-string content works for simple text results.
What if a tool takes minutes to run?
Same pattern. Anthropic waits for you to send the tool_result. There is no built-in timeout on your tool execution. Cap tool runtime yourself and return an error tool_result if you hit the cap.
Can I ignore some tool_use blocks?
No. Every tool_use needs a matching tool_result even if the result is “user declined” or “not implemented”. Skipping causes 400 errors on the next turn.
Does parallel tool use work?
Yes. Claude may return multiple tool_use blocks in one assistant message. Send all matching tool_results in the next user message.
Streaming with tools?
Yes. Use client.messages.stream(...) with tools. Access tool_use blocks after the stream completes via stream.get_final_message().
