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.
Abhijat · 2026-07 · 10 min read · 19 views · 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.
- #LangGraph
- #AI Agents
- #Reliability
keep reading —