ABHIJAT
← Back to Writing

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.

Abhijat2026-0710 min read4 views

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

LangGraphAI AgentsReliability