Writing errors an agent can act on
An error message is a prompt. It is the only instruction you get to inject at the exact moment the agent is deciding what to do next, and most systems waste it on a stack trace.
TL;DR: Every error you return is read by the model and shapes its next action, which makes error text the most consequential prompt in your system. Say what went wrong, whether retrying could work, and what to try instead. Never return a stack trace: it leaks internals, teaches nothing, and reliably produces a retry of the same failing call, delivered with fresh optimism.
Where you are. You have designed what tools accept and what they return when they succeed. Now the failure path, which matters more, because it decides whether a run recovers or starts circling.
An error message is a prompt
Here is the reframing that changes how you write them.
When a tool fails, your error text is appended to the message list and the model reads it before choosing its next action. So it is not a log line for you. It is an instruction, arriving at the moment of maximum influence, and it will be followed roughly as well as it is written.
That makes it strange that most systems return the exception's str().
What a bad error causes
Trace the failure through. KeyError: 'customer_id' goes into the context. The model has no idea what that means operationally. It has three options: repeat the call, guess a variation, or give up.
It usually repeats, because nothing told it not to. That is the ping-pong failure, and it was caused not by the model but by an error message that carried no information about what to do.
Meanwhile the stack trace has put your file paths, function names and internal structure into a context that may be logged, shown to a user, or influenced by untrusted input.
The three things an error must say
1. What went wrong, in operational terms. Not the exception class. "No order exists with id 4471" is a fact about the world. KeyError is a fact about your code.
2. Whether retrying could help. This is the field people omit and it decides the next action more than anything else.
- Transient, retry may work: a timeout, a rate limit, a temporary outage.
- Permanent for these arguments: not found, invalid format, no permission. Retrying identically is guaranteed to fail again, and saying so prevents it.
- Permanent, full stop: the tool is misconfigured. Stop and escalate.
3. What to try instead, when you know. This is where you can steer directly, and it is cheap because your code usually does know.
"No order found with id 4471. This looks like a customer number rather than an order id. Try
lookup_orders_for_customerinstead."
Compare that with KeyError. One produces a correct next call; the other produces a repeat.
Errors as guidance
Once you see errors as prompts, some become deliberate design rather than reporting.
Validation failures should teach the schema. "status must be one of: open, pending, closed. Received: 'in progress'." The model corrects on the next call. A bare "invalid value" produces another guess.
Too-large results should suggest narrowing. "Matched 4,812 records, too many to return. Add a date range or a status filter." This turns a dead end into a better query.
Permission denials should be honest and final. "This user cannot access billing records." Do not imply a retry might succeed, or the agent will spend steps trying.
What to keep out
Stack traces and internal paths. No value to the model, real cost in leaked structure.
Raw upstream errors. A third-party API's error body may contain its own text, and anything reaching the context is a place instructions can arrive. Translate upstream errors into your own vocabulary rather than forwarding them.
Anything that reveals data the user should not see. "No record found" is safer than "record exists but belongs to another tenant", and the second is a disclosure through an error message.
Do this before moving on
Take every tool you have and force each to fail in the two most likely ways: bad arguments, and the thing it depends on being unavailable. Print what lands in the message list.
Then check each against the three requirements: is it operational, does it say whether a retry could help, does it suggest an alternative where you know one. Most systems fail the second on every error, and adding it is often a one-line change per tool that measurably reduces wasted steps.
Go deeper
- Agent reliability covers the recovery behaviour these errors are feeding.
- Practice question: How do you recover from tool errors, and when do you give up? is the taxonomy above with a retry policy per class.
- Practice question: How do you make an agent survive tool failures? is the system-level version, including circuit breakers.
- Practice question: Your agent will not terminate is what uninformative errors cause, seen from the other end.
- Practice question: How do you defend an agent against prompt injection? is why forwarding a raw upstream error body is a bad idea.
Key takeaways
- Error text is a prompt read at the moment of maximum influence. Write it as an instruction, not as a log line.
- Say what went wrong operationally, whether a retry could help, and what to try instead.
- The retryable classification is the field most often omitted and the one that most changes the next action.
- Keep stack traces, internal paths and raw upstream error bodies out of the context.
Check yourself before an interviewer does. Answer from memory first.
A tool returns `KeyError: 'customer_id'` to the agent. What is the most likely next action?
