When Claude calls one of your tools, the API expects your next message to carry the answer. If anything else sits in between, or the answer is in the wrong place inside the message, the request is rejected before the model reads it. This post shows the shape the Claude tool use documentation requires, and a small loop that keeps it.
The error you will see
The Handle tool calls page names the message to look for: “tool_use ids were found without tool_result blocks immediately after”. The same page says that putting text before a tool_result in the user message causes a 400 error.
Both come from one rule. An assistant turn that ends with stop_reason: "tool_use" is waiting for answers, and the very next user turn has to supply them.
The three formatting rules
The documentation lists them under “Important formatting requirements”:
- Tool result blocks must immediately follow their corresponding tool use blocks. No message may sit between the assistant’s tool use message and the user’s tool result message.
- In the user message that carries the results, the
tool_resultblocks come first in the content array. Any text goes after all of them. - If the same assistant turn also called a server tool that has no result block yet, the user message must contain only
tool_resultblocks.
Each tool_result points back to its call through tool_use_id. That value must equal the id of the tool_use block it answers.
A rejected turn and a correct one
The docs give this turn as an example that causes a 400 error, because the text comes first:
{
"role": "user",
"content": [
{"type": "text", "text": "Here are the results:"},
{"type": "tool_result", "tool_use_id": "toolu_01", "content": "15 degrees"}
]
}
The fix is to swap the order:
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_01", "content": "15 degrees"},
{"type": "text", "text": "What should I do next?"}
]
}
The other common breakage is structural. A reminder from your app, a queued user message or a debug note gets appended to the history between the assistant turn and the results. That breaks the first rule even when every block is well formed.
One loop that keeps the shape
Build the result turn in one place, right after the response arrives, and append it before anything else touches the history. In this sketch, run_tool is your dispatcher and returns a string.
response = client.messages.create(
model=MODEL, max_tokens=1024, tools=tools, messages=messages
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": run_tool(block.name, block.input)})
except Exception as exc:
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"{type(exc).__name__}: {exc}",
"is_error": True})
messages.append({"role": "user", "content": results})
Three details carry the weight. Appending the full response.content keeps the tool_use blocks and their ids in the history. Every tool_use block gets a result, including the ones that failed. Any extra text for the model goes into results after the loop, never before it.
Several calls in one turn
Claude may call more than one tool in a single response. The parallel tool use page says to return one tool_result for each tool_use block, all together in the next user message, with every tool_result before any text. The page also says the API does not prescribe an execution order, so you can run the calls concurrently or one after another.
If you choose not to run one of the calls, for example because an earlier call in the batch failed, the page says to still return a result for it with is_error: true and a short explanation:
{
"type": "tool_result",
"tool_use_id": "toolu_02",
"is_error": true,
"content": "Not executed: the preceding write_file call failed."
}
The same page lists a separate user message for each result as the wrong format. The effect it names is that this “teaches” Claude to avoid parallel calls. The page does not say that form returns an error, so it is a different failure from the 400 above, and still worth avoiding.
Errors go inside the result
When a tool fails, the Handle tool calls page shows the error text in content with "is_error": true. Claude then works the error into its reply. The page recommends instructive messages, such as “Rate limit exceeded. Retry after 60 seconds.”, in place of a bare “failed”. A history that drops the result because the tool crashed is a common way to end up with an orphaned tool_use id.
Where this advice stops
- Server tools. Claude runs these itself, and the docs say you do not need to handle
is_errorresults for them. When one response mixes a clienttool_usewith aserver_tool_usethat has no result yet, reply with only the clienttool_resultblocks and keep the sametoolsarray. - Computer use and browser use. A result for a member of these toolsets must also echo the
toolset_namefrom thetool_useblock, or it is rejected. Its content is limited totextandimageblocks, and a browser use result may add onebrowser_stateblock. - Tool Runner. The SDK’s Tool Runner manages this loop and the result formatting for you. The manual pattern is for cases where you need direct control over execution.
- Histories from other APIs. The docs note that the Claude API has no separate
toolorfunctionrole. Results travel insideusermessages, so a history built for an API with a tool role needs converting first.
The pages linked here do not publish a full list of error strings for malformed histories. Match on the 400 status, read the message, and check the three rules above.

The Campfire
No commentsNobody has pulled up a log by this one yet. Be the first to say what you make of it.
Held for the desk. It appears after a look.