BadRequestError 400: tool_use ids were found without tool_result blocks (Anthropic)
After the model calls a tool, your very next message must return a tool_result for every tool_use id. Rebuild the conversation so the pairs match exactly.
Updated
The error
anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01A09q90qw90lq917835lq9. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}}The mirror-image violation also occurs:
'message': 'messages.2: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01A09q90qw90lq917835lq9. Each `tool_result` block must have a corresponding `tool_use` block in the previous message.'
What it means
Tool calling is a strict conversational protocol. When the model wants to use a tool, its assistant message contains tool_use blocks, each with a unique id. Your job: run the tools, then send one user message whose content starts with a tool_result block for every id, matching exactly. The API validates the pairing on each request and rejects any conversation where a tool_use is left unanswered, answered late, or answered with an unknown id.
Why it happens
The pairing breaks in predictable ways: the assistant message with the tool_use blocks was not appended to history before sending the results; a plain-text user message was inserted between call and result; the model called several tools in parallel and only one got a result; results were sent with a stale or mistyped id; or an exception during tool execution skipped the result entirely — errors need results too.
How to fix it
1. Follow the canonical loop shape.
resp = client.messages.create(model=MODEL, max_tokens=1024,
tools=tools, messages=messages)
if resp.stop_reason == "tool_use":
# 1) append the assistant turn UNCHANGED
messages.append({"role": "assistant", "content": resp.content})
# 2) build one tool_result per tool_use, ids copied
results = []
for block in resp.content:
if block.type == "tool_use":
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
# 3) send them as the next user message
messages.append({"role": "user", "content": results})
resp = client.messages.create(model=MODEL, max_tokens=1024,
tools=tools, messages=messages)2. Answer every id, even on failure. A tool that crashed still owes a result:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {exc}",
"is_error": True,
})The model reads the error and can recover; a missing result kills the whole conversation instead.
3. Keep tool_result blocks at the start of the user message content. Commentary text, if any, goes after them in the same message — never as a separate message in between.
4. When debugging, print the conversation skeleton.
for i, m in enumerate(messages):
kinds = [b.get("type", "text") if isinstance(b, dict) else b.type
for b in m["content"]] if isinstance(m["content"], list) else ["text"]
print(i, m["role"], kinds)The broken pair is visible immediately: a tool_use whose next message is not a user message leading with tool_result.
How to prevent it
Centralise history mutation in one function that appends call-and-result as an atomic pair — never let application code push messages directly mid-tool-loop. Test the multi-tool case early: parallel tool calls are where hand-rolled loops usually break first.
Related errors
- Invalid schema for function — the declaration-time half of tool errors
- JSONDecodeError: Expecting value
- max_tokens: Field required (Anthropic)
- This model's maximum context length is N tokens — long tool loops eventually hit it