What should an agent tool return when it fails?
A sentence the model can act on. A thrown exception stalls it, and 'validation failed' tells it nothing it can retry with.
It should return a plain string naming the specific thing that was wrong and showing what a valid call looks like, not a thrown exception or a generic "validation failed". Since the model typically gets one attempt to correct itself, the error is effectively an instruction: state the bad value, the expected format, and a worked example,…
An agent calling a tool is not a person reading a stack trace. It has no console to scroll, no colleague to ask, no ten minutes to think it over. It has the text you gave it back, and whatever it does next depends entirely on what that text says. This is the whole problem with error handling for tools: most of it is written for humans debugging in an editor, and reused, unchanged, for a model that gets one shot at fixing its own mistake.
Throwing is the worst option
When a tool throws, the agent typically sees something like "Error: Cannot read property 'id' of undefined" or a wrapped exception with a stack trace attached. This tells the agent that something failed. It does not tell the agent what it did wrong, or what to try instead. Faced with this, the model tends to do one of two things: repeat the same call, expecting a different result, or give up and report failure to the user. Neither is useful, and neither is the model's fault. You handed it an opaque signal and asked it to reason about a problem it cannot see.
A thrown exception is a debugging artefact. It is fine for a developer reading logs. It is close to useless as an instruction to an agent, because it was never written as one.
Validate loosely in the schema, strictly in the handler
Tool schemas are tempting places to put validation, and a strict schema feels like good practice. Mark every field required. Add a regex to the string. Set a tight enum. The trouble is that a schema violation usually fails before your code ever runs, and the message the agent gets back is generated by whatever validation library you are using, not by you. That message rarely explains itself well, and a strict schema can stall an agent outright, with no path to a retry.
The more useful split is: keep the schema loose enough that most reasonable calls pass it, and do the real checking in the handler, where you control the message. A field can be typed as a plain string in the schema and checked properly, with a clear explanation, once the handler has it. This costs you a little type safety at the boundary. It buys you the ability to write an error the agent can actually use.
What a good error string contains
A useful tool error has two parts: what was wrong, and what a valid call looks like. Leave either part out and you have given the agent half a fix.
"What was wrong" needs to name the actual field and the actual value, not a category. "Invalid input" is a category. "start_date must be before end_date, got start_date: 2024-03-01, end_date: 2024-02-15" is not.
"What a valid call looks like" is the part most tools skip. It is the difference between telling someone they are wrong and telling them how to be right. A short example, inline in the string, does this cheaply.
Before and after
Take a tool that books a meeting room. A caller sends a duration of zero.
throw new Error("Invalid duration");
The agent knows the duration was rejected. It does not know why zero is wrong, whether the unit is minutes or seconds, or what range is acceptable. It will likely retry with a slightly different number and hope.
return {
error: "duration_minutes must be an integer between 15 and 480. " +
"Received 0. Example valid call: " +
'{ "room": "Oak", "duration_minutes": 30, "start": "2024-03-01T09:00" }'
};
This version gives the agent a range, a type, and a worked example with the other fields filled in correctly. The next call it makes has a real chance of succeeding, because you have told it, in plain terms, what success looks like.
The same logic applies to missing or malformed fields. "Missing required field" forces the agent to guess which field and what format. "start must be an ISO 8601 datetime string, for example 2024-03-01T09:00:00Z. No start field was provided" does not.
Rate limits and quotas are a different kind of failure
Not every failure is the agent's fault, and the error text should say so when it isn't. A rate limit or quota error is not a mistake to correct: it is a state to wait out or a decision to hand back. The message should say which of those applies. "Rate limited, retry after 20 seconds" gives the agent a concrete action and a number to act on. "Monthly quota exhausted, this requires a plan upgrade" tells the agent, correctly, that no retry will help, and that the right move is to stop and tell the person it is working for.
Conflating these with ordinary validation errors is a common mistake. An agent that treats a quota error as a fixable input problem will burn several calls trying to reword a request that was never wrong in the first place.
One chance to self-correct
In practice, a model usually gets one attempt to read your error and fix its own call before the conversation moves on, the user loses patience, or the agent settles for reporting failure. That single attempt is entirely shaped by the string you returned. There is no follow-up question, no clarification round, no chance to ask what "invalid" meant. The error text is the whole conversation.
Writing tool errors well is, in this sense, closer to writing documentation than writing exception messages. You are not recording what went wrong for your own benefit later. You are giving a reader, right now, with one attempt remaining, exactly what they need to succeed on the next try.
Frequently asked questions
Should a tool error ever include a stack trace?
What if the failure genuinely isn't something the agent can fix, like a missing permission?
Does this matter for internal tools that only your own agents call?
Turn this guide into a skill your agent can run
Stop re-explaining the same workflow. Loreto packages it as a Claude Code skill from any source.