toolcall() ← all concepts

// concept · agentic

Your tool said failed. That was the whole message.

A tool call fails, your handler catches it, and you send back the word "failed". The agent tries the identical call again. Then again. Nothing is broken — you just never told it anything it could act on.

// the error is the steering

There is no side channel between a failed tool call and the model's next move. The result block you send back is the input to the next decision, and everything the model knows about the failure is the string inside it.

{ "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "failed", # ← the entire steering signal "is_error": true }

And the budget for getting it right is smaller than most people assume:

If a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user.

Two or three. Each one steered by nothing but the message you wrote. A message that carries no new information produces the same attempt, which means a generic error does not merely fail to help — it burns the recovery attempts you had.

Write it like an instruction

The guidance is unusually blunt, and it picks "failed" as its own anti-example:

Write instructive error messages. Instead of generic errors like "failed", include what went wrong and what Claude should try next (for example, "Rate limit exceeded. Retry after 60 seconds."). This gives Claude the context it needs to recover or adapt without guessing.

The test is simple: read your error message and ask whether it describes a different next attempt. If it does not, the retry will be identical.

failed Error: request unsuccessful 500 # → same call, again Rate limit exceeded. Retry after 60 seconds. Missing required 'location' parameter. Invalid departure date: must be in the future. Current date is 08/08/2025. No user with that email. Try search_users with a partial name instead. # → a different call

Note what the good ones share: a fact about the world, and a next move. The last one even redirects to a different tool, which is a move the model cannot invent from a status code.

Flag it, so it is not read as data

Set is_error: true on the result. Without it, an error string is just a string — a tool that returns "ConnectionError: the weather service API is not available (HTTP 500)" as an ordinary result is handing the model something that looks like an answer.

{ "type": "tool_result", "tool_use_id": "toolu_01A...", "content": "ConnectionError: the weather service API is not available (HTTP 500)", "is_error": true }

With the flag set, the failure is unambiguous and the model can explain it rather than reason from it — the docs' example response being "I'm sorry, I was unable to retrieve the current weather because the weather service API is not available. Please try again later."

Server tools are not yours to handle. When an Anthropic-hosted tool such as web search fails, "Claude will transparently handle these errors and attempt to provide an alternative response" — you do not return is_error for those.

Delete the class instead of apologising for it

One whole category of failure — malformed inputs, missing parameters, type mismatches — does not need a good error message, because it does not need to happen.

To eliminate invalid tool calls entirely, use strict tool use with strict: true on your tool definitions. This guarantees that tool inputs will always match your schema exactly, preventing missing parameters and type mismatches.
{ "name": "get_weather", "description": "...", "input_schema": { ... }, "strict": true }

And if the model keeps reaching for the wrong tool rather than calling it wrongly, the error message is the wrong layer entirely — the docs' first suggestion there is "more-detailed description values in your tool definitions". That is a different fix, applied before anything runs.

Two kinds of error, and only one is recoverable

MCP draws the same line from the protocol side, and the distinction is worth carrying into your own handlers.

execution errors isError: true, in the result API failures · validation · business logic "actionable feedback that language models can use to self-correct and retry with adjusted parameters" protocol errors a JSON-RPC error unknown tool · malformed request · server error "issues with the request structure itself that models are less likely to be able to fix"

Hence the spec's asymmetric advice: clients SHOULD pass execution errors to the model to enable self-correction, but MAY pass protocol errors, "though these are less likely to result in successful recovery".

The practical version: if the failure is something a different set of arguments would fix, say what they are. If it is not — the tool does not exist, your server is down — no wording will make the retry work, and the honest move is to stop the loop rather than spend its attempts.

Related: idempotent tools covers what a retry does to your data, which is the other half of this same moment; nothing is telling your agent to stop covers the loop those attempts run inside.

Sources: Claude Platform docs — Handle tool calls; Model Context Protocol specification (draft), Tools → Error Handling. Verified 2026-09-13.

One concept a week. Free.

The deeper, copy-paste version of each ToolCall short — in your inbox.

// total: 0.00 · spam: void · unsubscribe: one click