Unit 05.02: Tools that are safe to fail
A tool that raises kills the run. A tool that returns a failure lets the graph decide what to do - which is the whole point of having a graph.
One envelope shape for every tool
Rather than each tool raising its own exception type, every tool returns the same three fields: whether it succeeded, an error string, and a value. One error branch then handles all of them.
The code contrasts a raising tool with an enveloped one.
def fetch_rate(currency):
"""A tool that raises is a tool that kills the run."""
rates = {"USD": 1.0, "EUR": 0.92}
return rates[currency]
def fetch_rate_safe(currency):
"""Returns a result envelope instead. The graph decides what to do."""
rates = {"USD": 1.0, "EUR": 0.92}
if currency not in rates:
return {"ok": False, "error": f"no rate for {currency}", "value": None}
return {"ok": True, "error": "", "value": rates[currency]}
try:
fetch_rate("JPY")
except KeyError as exc:
print(f"unsafe: raised {type(exc).__name__} -- the run is over")
print("safe :", fetch_rate_safe("JPY"))
print("safe :", fetch_rate_safe("EUR"))
# The envelope shape -- ok, error, value -- means every tool fails the same way,
# so one error branch in the graph handles all of them. Tools that each raise
# their own exception type need a handler per tool.
The unsafe version raises KeyError and the run is over - including whatever earlier nodes had already accomplished. The safe version returns a value the router can read.
The uniformity is what pays off at scale. Ten tools that each raise their own exception need ten handlers and a catch-all that will eventually hide something. Ten tools returning the same envelope need one branch, and adding an eleventh tool requires no change to the graph's error handling at all.
The mistake this prevents
The mistake is wrapping every tool call in a try/except at the call site. It works and it scatters the error contract across the codebase, so no single place tells you how failures are represented. Put the envelope in the tool, once.
Takeaway
Give every tool the same result envelope - ok, error, value - so the graph has one error branch rather than one per tool. A raising tool discards the state of the whole run.
