Topic 7.5
Feeding Errors Back
In one line
A tool will fail: bad arguments, unknown records, outages. Catch every failure, turn it into a short tool message the model can read, and let the model correct itself, with a limit on repeats and without leaking internal details.
Think of it like this
A waiter takes your order to the kitchen and comes back saying 'sorry, the fish is finished'. A good waiter does not vanish, and does not keep walking to the kitchen with the same order. He tells you what went wrong, you pick something else, and after two refusals he suggests you look at the menu again. The error message is information you use.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Tool error
- A failure while running a tool, reported to the model as an ordinary tool message.
- Self-correction
- The model reading an error and calling the tool again with fixed arguments.
- Generic error
- A short message with no internal details, sent to the model for unexpected failures.
- Server log
- Where the full error and trace go, visible to you and not to the model.
- Retry limit
- The maximum times the same failing call is allowed before the loop gives up.
- Fallback text
- A fixed answer your code gives the user when the tool cannot be made to work.
Step by step
01Step 1: a failing call
The fake model asks for {"city":"Punee"}. The tool raises ToolError because the city is unknown.
02Step 2: turn the failure into a message
The loop catches ToolError and builds {"error":"unknown city: Punee. Known cities: Pune, Mumbai, Delhi"}. It is appended like any tool result, with the same tool_call_id.
03Step 3: the model recovers
With the error in front of it, the model calls again with Pune. In the example this is scripted. With a real model it usually works for clear errors and fails more often for vague ones.
04Step 4: unexpected failures stay private
The second example's tool raises an error mentioning an internal host and port. The loop prints it to the server log and sends the model only the tool failed unexpectedly.
05Step 5: stop repeating
The fake model never learns and asks for the same order each step. The loop counts failures per (tool, arguments). On the second identical failure it stops and returns the fallback text instead of looping to the step limit.
Try it yourself
- 1
Make the message worse
In the first example change the error text to just
error. A real model has far less to go on. Predict how a fix attempt could still go wrong, then write the message back in a helpful form. - 2
Change the limit
In the second example call
run(max_steps=6, max_same_failures=3). How many steps run now? What is the cost of each extra step (one more model call plus one more tool call)? - 3
A different failure
Make
lookup_orderraiseToolError("order A17 not found")for a known-bad id in the first example's style. Should the loop limit repeats for this kind of failure too?
Code & diagrams
The model's recovery is scripted. A real model decides it itself, and clear error messages make that more likely.
Expected output
step 1: model asks get_weather {"city":"Punee"}
step 1: tool message {"error":"unknown city: Punee. Known cities: Pune, Mumbai, Delhi"}
step 2: model asks get_weather {"city":"Pune"}
step 2: tool message {"city":"Pune","temp_c":31}
step 3: model answers: It is 31 C in Pune.The 'server only' line stands for your log system. The model never sees it.
Expected output
step 1: lookup_order {"order_id":"A17"}
log (server only): connection to db.internal:5432 refused
tool message: {"error":"the tool failed unexpectedly"}
step 2: lookup_order {"order_id":"A17"}
log (server only): connection to db.internal:5432 refused
tool message: {"error":"the tool failed unexpectedly"}
the same call failed 2 times: giving up
final: Sorry, I could not look up that order.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Send the raw exception to the model
In the second example, change the except block so content is json.dumps({"error": str(e)}) instead of the generic text, and run the Python version.
Myth vs fact
Myth
If a tool fails, the loop should raise an error to the user.
Fact
Often the model can fix the call itself. Report failures to the model first, and surface them to the user when recovery fails.
Myth
More detail in an error message is always better.
Fact
Detail that helps fix the call is good. Detail about your systems is a leak. Separate the two.
Myth
Retrying until it works is fine.
Fact
Repeating a call that fails for the same reason wastes money and can hammer a struggling service. Limit repeats.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Make errors structured:
{"error": {"code": "unknown_city", "message": "...", "valid": [...]}}. Codes let you count and alert on error types while the message guides the model. - ▸
Return a partial result with a note when part of a call succeeded ('3 of 4 orders found; A19 not found') rather than failing the whole call.
- ▸
Tell the model what it should do on a permanent failure: 'tell the user the order system is unavailable and do not guess'. Without it, some models make up a plausible answer.
Remember this
- 1
Why this exists. Tool calls fail all the time: the model sent a city that does not exist, an order id with a typo, a date in the wrong format, or the database was down. If your loop crashes, the user sees an error. If the loop swallows the failure and returns nothing, the model invents an answer. If it sends the failure back as a tool message, the model can often fix the call or explain the problem honestly. Errors are part of the conversation.
- 2
The pattern. Wrap every tool run in a try block. On failure, build a small message and append it as the tool result for that
tool_call_id, exactly like a success. JSON such as{"error": "unknown city: Punee. Known cities: Pune, Mumbai, Delhi"}works well. The model reads it and (usually) calls again with the typo fixed. The first example runs this exact exchange with a scripted fake model that recovers. - 3
Write messages for the model to act on. 'Error' is useless. 'unknown city: Punee. Known cities: Pune, Mumbai, Delhi' says what was wrong and what to do. Good messages name the argument, say what was expected and offer valid options when the list is short. This is the same idea as the repair loop for invalid JSON in Topic 4.6, applied to tools. The validator from Topic 7.4 already returns a list of such problems.
- 4
Two kinds of failure, two messages. A failure the model can fix (bad argument, not found) gets a precise message. An unexpected failure (a bug, an outage, a stack trace) should not reach the model: it may contain server names, ports or secrets, and the model can pass it to the user. Log the details on your side with the trace id, and send something generic such as 'the tool failed unexpectedly'. The second example logs
connection to db.internal:5432 refusedon the server and sends only the generic text. - 5
Limits on retries. A model that gets an error may try the same call again, and again. Count failures per tool and arguments (or per tool), and after a small number (two or three) stop: send a last message saying the tool is unavailable and ask the model to tell the user, or end the loop with your own fallback text. This is on top of the loop's overall step limit (Topic 7.2). Retrying a call that failed because of an outage only adds load (Topic 6.8 covers backoff for your own calls).
- 6
Honesty on the way out. When the tool cannot be made to work, say so to the user in plain words ('I could not look up that order right now'). Do not let the model fill the gap with a made-up answer: the fallback text, or an instruction in the last tool message not to guess, prevents that. Count tool errors in your metrics by tool and by kind (Phase 11): they tell you whether to fix the tool, the schema or the prompt.
Explain it without notes
Why should tool failures be returned to the model instead of raising an exception out of the loop?
How do you decide what error text the model gets and what stays in the logs?
How do you stop a model from retrying a failing tool call forever?
Practice
Write run_tool(registry, name, args) that returns a JSON string: the result as JSON on success, {"error": "<message>"} for ValueError, and {"error": "the tool failed unexpectedly"} for any other exception.
Write Limiter(max_failures) with a method failed(key) that returns True once the same key has failed max_failures times.
Trade-offs
- ↔
Detailed errors help the model recover and risk leaking internals; a separate server log solves both.
- ↔
More retries recover more calls and cost more time and tokens; a low limit fails faster and more often.
- ↔
Letting the model fix its own calls is flexible and can be slow; validating before the call (Topic 7.4) catches most mistakes cheaper.
Done when you can
Done when every tool run is wrapped and every failure becomes a tool message.
Done when error messages name the problem and the fix, and unexpected failures send only a generic message.
Done when the real error is logged on your side with enough to debug.
Done when repeated identical failures stop the loop with a fallback answer.