Designing Tool Errors for Agents

An agent looks up an order. It passes a customer name where the tool expects an order ID, and the tool answers with four words: "Error: invalid input." So the agent tries the same call again, unchanged. Then it tries a different tool, at random. Then it tells the user the order does not exist.
Nothing in that chain is a model failure, exactly. The order existed. The first call was fixable in one step. What failed was the error, because the error told the agent nothing it could act on, and an agent that cannot act on a failure will improvise around it. Retry the same call. Guess. Move on and report a conclusion the data never supported.
Plenty of design time goes on what tools return when they work. The failure returns deserve the same design attention, because for an agent the error is not a log entry. It is the next instruction.
An error is a decision point, not a log line
Code that calls an API typically branches on a status code, and pays little attention to the message. An agent, by contrast, reads the message and decides. That single difference changes what a good error looks like. A code can be terse because the branching logic lives in the caller, written in advance by a person who enumerated the cases. With an agent, the branching logic is being written in the moment, by the model, from whatever your error gave it to work with.
The protocols already separate two kinds of failure, and the distinction is worth keeping. The Model Context Protocol divides errors into protocol errors, for problems with the request itself such as an unknown tool or invalid arguments, and tool execution errors, reported in the result with the error flag set, for failures inside the work: an API that failed, input data that did not validate, a business rule that refused the operation. Anthropic's tool use has a parallel mechanism: your code returns the failure as a tool result with the is_error flag set, and the model reads that result and chooses what happens next.
That choice is the design surface. Fix the input and retry. Try a different tool. Check the state of the world before doing anything. Stop and ask a person. A well-built error makes exactly one of those moves look obvious. A vague one makes all four look equally plausible, and the model picks by vibes.

What a useful error actually contains
Start with what failed, stated in the terms of the task rather than the terms of the stack. "Order ORD-8841 was not found in live orders" is something an agent can reason about. A database exception naming an internal table is not, and it leaks structure you never meant to publish. Then name the input that caused it. One field, identified precisely, turns a vague retry into a targeted correction.
Say whether the same call can succeed. This is the piece most errors leave out, and it is the piece that prevents the retry loop. A corrected input can succeed. A transient failure can succeed if it is simply run again. A refused operation will not succeed no matter how many times it is repeated, and the error should say so plainly, because an agent told "permission denied, do not retry this operation" behaves very differently from one told "access error."
Then, where you can, say what to try instead. Anthropic's guidance on writing tools makes the same point: error responses should offer specific and actionable improvements rather than opaque codes or tracebacks, and its guidance notes that error responses can also give examples of correctly formatted inputs. "Dates must be ISO format, for example 2026-11-02" ends an entire class of guessing. So does "you passed a customer name; this tool takes an order ID — search by customer first, then call this tool with the ID it returns." You are not writing documentation here. You are steering the next step.
Keep it short. An error that runs to a paragraph of context competes with the task itself, and it will be read again on every retry. The model needs the failure, the field, the retry answer, and the next move. Everything else belongs somewhere else.
Keep two versions of every failure
The model is not the only reader, and it should not get the same text as the other one. Operators need the full picture: the raw exception, the request identifier, the timing, the stack. The model needs none of that, and some of it it must never see. Credentials, internal hostnames, query text containing another customer's data — a raw traceback can carry all three straight into a conversation that may be logged, summarised, or shown to a user.
So build the split into the tool wrapper, not into each tool. The wrapper catches the raw failure, writes the complete version to the operator log, and composes the short version for the model from the classification: what failed, which field, whether a retry can work, what to try instead. Stripe's API errors are a useful model for the shape: a machine-readable type and code, a human-readable message, and, when the error is parameter-specific, a param field naming the exact input at fault. Separate fields for separate readers. An agent can branch on the code while reasoning over the message, and a person debugging later gets both.
The hardest case in this split is the timeout after a side effect. The call may have completed. The payment may have posted, the email may have sent, the record may already exist — the agent simply never heard back. An error that says "timed out, retry" invites a duplicate. The honest version says the state is unknown and names the check to run first: look up the record, then decide. That one sentence is the difference between a recoverable failure and a doubled action a person has to unwind by hand.
Here is that shape as a worked example, with sample values:

Where better errors stop helping
An honest limit: a good error cannot rescue a badly drawn tool. If two tools overlap and the agent picked the wrong one, a polished message from the wrong tool still leaves it in the wrong place. If the tool itself is missing a capability the task needs, no wording invents it. Error design assumes the toolset is sound. It fixes recovery, not selection.
Hints can also be wrong. If your wrapper generates a suggested correction from a template that has drifted from the real validation rules, the model will follow it confidently, fail again, and now be stuck in a loop your own message built. Generate hints from the same validation state that rejected the call, never from a separate description of it. And test recovery the way you test success: an eval set for a tool (evals being the fixed set of tasks you score the agent against) should include calls designed to fail, scored on the agent's next move. Did it correct the field, or retry unchanged? Did it check state after the ambiguous timeout, or fire the call again? Recovery rate is a number you can measure, which means it is a number you can improve.
Some failures, finally, should give the model almost nothing. An authorisation boundary is not a puzzle to route around. "This operation is not available in this deployment. Continue without it, or ask the user" is a complete error. Anything more detailed teaches workarounds you do not want taught.
Design the success paths and you get an agent that works until something breaks. Design the failure paths too — what each error says, who else reads it, and which next move it points to — and you get an agent whose bad moments are short, legible, and usually self-corrected. In production, that second property is most of the job.
Work with us
If your agents loop on the same call, guess after a failure, or quietly give up on tools that actually work, the problem is often in what the tool said when it failed. Bring us the deployment where recovery breaks down through Trida AI's intake.
