When a Tool Call Fails: Beginner Error Handling
Your agent works when everything goes right. But tools fail: the math expression is malformed, a website is down, an argument is missing. A good agent does not crash when that happens. It is told about the failure and gets a chance to try something else.
This lesson shows the beginner-friendly way to handle a failing tool, which is one of the things that separates a toy agent from a useful one.
What You'll Learn
- The two kinds of failure: your code crashing versus a tool returning an error
- How to catch an error and report it back to the model
- The
is_errorflag and why it helps - Why letting the model see the error makes the agent more capable
Two ways a tool call can go wrong
There are two different failures, and you handle them differently:
The goal is to turn every crash into a message the model can read and react to.
| Criteria | Tool returns an error | Your code crashes |
|---|---|---|
| What happens | The function returns an error message | An unhandled exception stops the program |
| Example | calculator('two plus two') returns 'unsupported' | calculator(None) raises TypeError |
| Who finds out | The model, on the next turn | Nobody, your whole agent halts |
| Fix | Send the message back as a tool_result | Wrap the call in try/except |
Tool returns an error
- What happens
- The function returns an error message
- Example
- calculator('two plus two') returns 'unsupported'
- Who finds out
- The model, on the next turn
- Fix
- Send the message back as a tool_result
Your code crashes
- What happens
- An unhandled exception stops the program
- Example
- calculator(None) raises TypeError
- Who finds out
- Nobody, your whole agent halts
- Fix
- Wrap the call in try/except
The whole skill here is converting the second column into the first: catch crashes and turn them into a tool result the model can see.
Catch the crash with try/except
The fix is one try/except around the place where you run the tool. Instead of letting an exception kill your agent, you capture it and turn it into an error result.
for block in response.content:
if block.type == "tool_use":
try:
fn = TOOLS[block.name]
output = fn(**block.input)
is_error = False
except KeyError:
output = f"Error: no tool named '{block.name}'."
is_error = True
except Exception as e:
output = f"Error running {block.name}: {e}"
is_error = True
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
"is_error": is_error,
})
Two things changed from Lesson 4:
- The tool call is wrapped in
try/except, so a crash becomes a string instead of stopping the program. - The
tool_resultnow carries"is_error": Truewhen something went wrong.
Why the model needs to know
This is the key idea. When you send the error back as a tool_result instead of crashing, the model gets a chance to recover. It sees "Error: unsupported characters in expression" and can rewrite the expression, or explain to the user that it cannot do that, or try a different tool.
An agent that crashes on the first bad input is fragile. An agent that reports its failures and tries again is the thing you actually want. The think-act-observe loop from Lesson 1 includes observing the bad observations too.
The is_error flag is a small but useful signal. Setting it tells the model clearly "that attempt failed," which nudges it toward fixing the problem rather than treating the error text as data.
Make the tool itself defensive
Catching exceptions in the loop is your safety net. But it is even better when the tool returns a clear error on its own, so the model gets a precise message. Compare a fragile tool with a defensive one:
# Fragile: crashes on bad input, the model learns nothing useful.
def calculator(expression):
return str(eval(expression))
# Defensive: returns a clear message the model can act on.
def calculator(expression):
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return "Error: only digits and + - * / ( ) are allowed."
try:
return str(eval(expression))
except Exception as e:
return f"Error: could not evaluate expression ({e})."
The defensive version never raises; it always returns a string. That string, whether a result or an error, flows straight back to the model as an observation. Defensive tools plus a try/except net in the loop give you two layers of protection.
A small but important safety note
Notice the calculator restricts input to digits and basic operators before evaluating. That check is there for safety, not just correctness: eval will run arbitrary code, and the model's input should never be trusted blindly. Whenever a tool touches something powerful (your files, the shell, an email, a payment), validate the input first and consider asking for confirmation before acting. The model proposes; your code decides whether the action is safe to run.
Practice handling a failure (runs in your browser)
Run this to see the difference between a crash and a handled error. The first call succeeds, the second would crash a naive agent, but here it is caught and turned into a clean error result.
Both calls return a string the model can read. Neither crashes the program. That is exactly what you want feeding back into the loop.
Key Takeaways
- A tool can fail two ways: returning an error string, or crashing your code with an exception.
- Wrap the tool call in
try/exceptso a crash becomes atool_resultinstead of halting the agent. - Set
"is_error": Trueon the result so the model clearly knows the attempt failed. - Sending the error back lets the model recover: rewrite the input, try another tool, or explain the limit to the user.
- Write defensive tools that validate input and never trust it blindly, especially for anything powerful.

