AI Engineering
Modeling agent retries as a graph instead of a loop
A while-loop with a retry counter works until the failure modes multiply. LangGraph's state machine model handles that better than it first looks like it should.
Last updated September 17, 2026
The first version of most agent retry logic is a while-loop with a counter: try the call, if it fails, increment, try again, give up after N attempts. That's fine for one failure mode. It stops being fine the moment an agent can fail in more than one way that each deserve a different response — a malformed tool call should be retried with a corrected prompt, a rate limit should back off and wait, a genuinely unrecoverable error should stop and surface to a human. A single retry counter can't express any of that distinction.
Failure as a state, not an exception
The shift that makes LangGraph's model click is treating a failed step's output as just another state the graph can be in, not an exception that unwinds the stack. A tool call that returns malformed output isn't a crash — it's a node that produced a {status: "invalid", error: "..."} result, which is exactly as valid a piece of graph state as a successful result. That framing matters because it means the next step gets to look at that state and decide what to do, instead of a generic catch block deciding for it.
def call_tool(state):
try:
result = tool.invoke(state["input"])
return {"status": "ok", "result": result}
except ValidationError as e:
return {"status": "invalid", "error": str(e)}
except RateLimitError:
return {"status": "rate_limited"}
Conditional edges are the retry policy
With failure represented as state, the graph's conditional edges become the actual retry policy, expressed as routing logic instead of buried inside a try/except:
def route_after_tool_call(state):
if state["status"] == "ok":
return "next_step"
if state["status"] == "invalid":
return "repair_and_retry" # feed the error back into the prompt
if state["status"] == "rate_limited":
return "backoff_and_retry" # wait, then retry the same call
return "escalate_to_human"
Each failure mode gets its own edge, its own downstream node, and its own retry count if it needs one — a malformed tool call and a rate limit don't have to share a counter, because they're not the same problem with the same fix.
Where the complexity earns its keep
This is more setup than a while-loop for a single, simple failure mode, and it's not worth it for that case. It earns its keep once an agent has real branching failure behavior: different retry strategies per error type, a cap on repair attempts that's separate from a cap on rate-limit backoffs, or a path that escalates to a human after some failures but silently retries others. At that point the graph isn't extra complexity layered on top of a loop — it's the actual shape of the retry policy, made explicit instead of encoded as nested conditionals inside one big function.
The practical test for whether you need this: if you can describe your retry logic in one sentence ("retry up to 3 times, then give up"), a loop is the right tool and a graph is overkill. If your retry logic needs a paragraph with the word "unless" in it more than once, you already have a graph — the only question is whether you're going to write it down as one.
Tags
Related posts