Skip to content

Claude API "tool_use ids were found without tool_result blocks immediately after": Fix ​

Your agent request fails with HTTP 400 invalid_request_error: tool_use ids were found without tool_result blocks immediately after. The problem is your message history, not the model. Below: the ordering rules the API enforces, the code paths that break them, and a loop plus a validator that keep them intact.

Quick answer

Every tool_use block in an assistant message must be answered by a tool_result with the matching tool_use_id in the very next message, a user message — nothing in between, and the tool_result blocks first in its content, before any text. The error means at least one tool_use id has no result in the right place. Usual causes: a tool that threw and was skipped, only one of several parallel calls answered, user text inserted before the results, or history trimming that split a call from its result. Fix the history; retrying the same request fails the same way.

Symptoms and What the Error Means ​

The core phrase is quoted in Anthropic's tool-use docs; the full message, as it appears in public error reports, names the offending message index and ids:

json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.3: `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 messages.N prefix is a zero-based index into your messages array — in reported cases, the assistant turn whose call went unanswered — so you can jump straight to it in your logs.

Related errors from the same family:

  • The mirror case (reported wording): unexpected `tool_use_id` found in `tool_result` blocks: toolu_... — a result exists, but the assistant turn that requested it is missing or not directly before it.
  • Unresolved server tools (official wording): `web_search` tool use with id `srvtoolu_...` was found without a corresponding `web_search_tool_result` block — you added content after the results while a server tool call was pending.
  • Modified thinking blocks (official wording): `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified — the thinking blocks of the calling turn were altered or dropped.

The Rules the API Enforces ​

From Anthropic's "Handle tool calls", "Parallel tool use" and "Stop reasons" pages:

  1. Results immediately follow calls. No messages between the assistant's tool_use message and the user's tool_result message.
  2. Results come first. In that user message, tool_result blocks come first in the content array; any text goes after all of them.
  3. One result per call. One tool_result for every tool_use block, matched by tool_use_id.
  4. Parallel calls share one message. Return all results together in a single user message; per Anthropic, separate messages "teach" the model to stop calling tools in parallel.
  5. Pending server tools need a results-only message. If the turn also called a server tool (such as web search) with no result yet, the user message must contain only tool_result blocks, and the tools array must stay the same.
  6. Thinking travels with the call. Include the assistant turn's thinking blocks unmodified, signature included.

One nuance: the Messages API combines consecutive user turns into one. So splitting results across two user messages may not error by itself — but a text-only user message placed before the results gets merged in front of them and breaks rule 2.

Why It Happens ​

  1. A tool raised an exception and the loop skipped it instead of returning an error result.
  2. Only the first of several parallel calls was answered — the code read response.content[0] or stopped at the first match.
  3. The user interjected while a tool ran, and the app appended that text before the results.
  4. History trimming dropped an assistant turn but kept its results, or the reverse.
  5. A truncated response was stored. With stop_reason: "max_tokens", the last block can be an incomplete tool_use; Anthropic's advice is to retry with a higher max_tokens, not keep the partial call.
  6. An interrupted run was saved mid-loop — a crash, a cancel or a dropped stream after the assistant turn but before the results.
  7. Format conversion bugs between OpenAI-style tool_calls / role: "tool" messages and Anthropic blocks dropped, reordered or mismatched ids.

Fix 1: A Tool Loop That Always Answers Every Call ​

Return a result for every tool_use, including failures, in one user message:

python
import json
import anthropic

client = anthropic.Anthropic()

def run_loop(messages, tools, model="claude-sonnet-5-5", max_tokens=4096):
    while True:
        resp = client.messages.create(model=model, max_tokens=max_tokens,
                                      tools=tools, messages=messages)
        # Incomplete tool call: don't store it, retry with more room
        if (resp.stop_reason == "max_tokens" and resp.content
                and resp.content[-1].type == "tool_use"):
            max_tokens *= 2
            continue
        # Keep every block (thinking included) exactly as returned
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason != "tool_use":
            return resp
        results = []
        for block in resp.content:
            if block.type != "tool_use":
                continue
            try:
                output = run_tool(block.name, block.input)   # your dispatcher
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": json.dumps(output)})
            except Exception as e:
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": f"Error: {e}", "is_error": True})
        # One user message, tool_result blocks only
        messages.append({"role": "user", "content": results})

Notes:

  • is_error: true is the documented way to report a failure; Claude reads it and can retry or explain. Write instructive messages ("Rate limit exceeded. Retry after 60 seconds."), not "failed".
  • If the user typed something while tools ran, append it after the results, or send it as the next user turn.
  • Cap the max_tokens doubling in production.
  • The SDKs also ship a Tool Runner that manages this loop for you; use it unless you need custom control.

Fix 2: Validate History Before Every Request ​

A quick check catches broken histories before the API does (plain dicts; call .model_dump() on stored SDK blocks):

python
def blocks(msg):
    c = msg["content"]
    return [] if isinstance(c, str) else c

def check_tool_pairs(messages):
    problems = []
    for i, msg in enumerate(messages):
        if msg["role"] != "assistant":
            continue
        ids = {b["id"] for b in blocks(msg) if b.get("type") == "tool_use"}
        if not ids:
            continue
        nxt = messages[i + 1] if i + 1 < len(messages) else None
        if nxt is None or nxt["role"] != "user":
            problems.append(f"messages.{i}: no user message with results follows")
            continue
        leading = []
        for b in blocks(nxt):
            if b.get("type") != "tool_result":
                break               # results must come first
            leading.append(b["tool_use_id"])
        missing = ids - set(leading)
        extra = set(leading) - ids
        if missing:
            problems.append(f"messages.{i}: unanswered {sorted(missing)}")
        if extra:
            problems.append(f"messages.{i + 1}: results for unknown ids {sorted(extra)}")
    return problems

Run it in tests and log its output in production. An empty list means the ordering rules above hold.

Fix 3: Repair a Conversation That Is Already Broken ​

When a stored session is stuck — every new message fails with this 400 — patch the history instead of discarding it:

  • Missing results: insert a tool_result for each unanswered id, with "is_error": true and content such as "Tool call was cancelled.", first in the user message right after that assistant turn.
  • Orphaned results: remove tool_result blocks whose id is not in the immediately preceding assistant message.
  • Text before results: move it after the tool_result blocks.
  • Trimming for length: cut only at a plain user turn; the prompt is too long page has a helper that does this.

Leave the assistant turn's thinking blocks byte-for-byte unchanged while repairing; editing them produces a different 400.

Fix 4: Getting the OpenAI-Compatible Format Right ​

On an OpenAI-compatible endpoint the same rule is written differently. Each entry in the assistant's tool_calls needs a role: "tool" message with the matching tool_call_id, and all of them must come before the next user or assistant message:

json
[
  {"role": "assistant", "content": null, "tool_calls": [
    {"id": "call_1", "type": "function",
     "function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}},
    {"id": "call_2", "type": "function",
     "function": {"name": "get_weather", "arguments": "{\"city\": \"Tokyo\"}"}}
  ]},
  {"role": "tool", "tool_call_id": "call_1", "content": "18°C, cloudy"},
  {"role": "tool", "tool_call_id": "call_2", "content": "24°C, clear"}
]

A missing tool message, or a user message wedged between them, is the OpenAI-format version of the same mistake.

If You're Calling Claude Through byesu ​

byesu is an AI API gateway with an OpenAI-compatible endpoint (https://byesu.com/v1) and an Anthropic-native endpoint (/v1/messages on the same host), billed pay-as-you-go with no subscription. Two points matter for this error:

  • Both formats carry the same rule. On /v1/messages, follow the block rules above; on /v1/chat/completions, the tool_calls / role: "tool" pairing from Fix 4. Through the OpenAI-compatible endpoint the message may arrive wrapped in an OpenAI-style error object; search for the tool_use ids were found without tool_result blocks text.
  • Expect parallel calls. claude-opus-5-5, claude-sonnet-5-5 and claude-haiku-5-5 all support parallel tool use, so answer every call of a turn in one message. Note that Claude Opus 5.5 and Sonnet 5.5 reject forced tool use (tool_choice of any or a named tool); use auto.

To get started, create a token in the console, point your SDK at the endpoint above, and run the loop from Fix 1. Full setup: Quick Start.

FAQ ​

What does "tool_use ids were found without tool_result blocks immediately after" mean? ​

It means an assistant message in your history contains a tool_use block that is not answered by a tool_result with the same tool_use_id at the start of the very next user message. The API validates the whole history on every request, so the request fails until you add the missing result, remove the extra message in between, or move text that was placed before the results.

Can I put text before tool_result blocks in a Claude API message? ​

No. In the user message that answers a tool call, the tool_result blocks must come first in the content array, and any text must come after all of them. Text placed first causes a 400. If the assistant turn also called a server tool that is still pending, the message must contain only tool_result blocks.

How do I return results for parallel tool calls? ​

Send one tool_result for every tool_use block in the assistant turn, all together in a single user message, each with the matching tool_use_id. Anthropic recommends this format; sending each result in a separate message reduces how often Claude makes parallel calls, and answering only the first call triggers this error.

What should I return if my tool fails or is cancelled? ​

Return a tool_result anyway, with is_error set to true and a short, instructive error message in content. Claude reads the error and can retry or explain the failure to the user. Skipping the result because the tool threw an exception is one of the most common causes of this 400.

What does "unexpected tool_use_id found in tool_result blocks" mean? ​

It is the mirror of this error: a tool_result refers to a tool_use_id that does not exist in the immediately preceding assistant message. It usually happens when history trimming removed the assistant turn but kept its results, or when ids were rewritten during format conversion. Remove the orphaned results or restore the assistant turn.

Got a problem? Contact support or join our group.