How to Fix 'agent infinite loop' in LangGraph (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
agent-infinite-looplanggraphtypescript

When LangGraph throws GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition, it usually means your graph kept routing back into itself and never reached an end node. In TypeScript, this often shows up after adding a conditional edge, a tool loop, or a state update that never changes the branch condition.

This is not a LangGraph bug. It means your graph topology or state transition logic is wrong, and the runtime did exactly what you told it to do.

The Most Common Cause

The #1 cause is a node that always routes back to itself or back into the same cycle without a termination condition. In practice, this happens when your conditional edge returns the same node name forever, or when your state never changes in a way that breaks the loop.

Here’s the broken pattern:

BrokenFixed
```ts
import { StateGraph, END } from "@langchain/langgraph";

type State = { messages: string[]; done?: boolean; };

const graph = new StateGraph<State>({ channels: { messages: { value: (x: string[], y: string[]) => x.concat(y), default: () => [] }, done: { value: (_x, y) => y, default: () => false }, }, });

graph.addNode("agent", async (state) => { return { messages: [...state.messages, "thinking..."], done: false, }; });

graph.addConditionalEdges("agent", (state) => { return state.done ? END : "agent"; });

const app = graph.compile(); |ts import { StateGraph, END } from "@langchain/langgraph";

type State = { messages: string[]; done?: boolean; };

const graph = new StateGraph<State>({ channels: { messages: { value: (x: string[], y: string[]) => x.concat(y), default: () => [] }, done: { value: (_x, y) => y, default: () => false }, }, });

graph.addNode("agent", async (state) => { const nextDone = state.messages.length >= 3;

return { messages: [...state.messages, "thinking..."], done: nextDone, }; });

graph.addConditionalEdges("agent", (state) => { return state.done ? END : "agent"; });

const app = graph.compile();


The broken version always sets `done: false`, so the conditional edge can never reach `END`. The graph keeps re-entering `agent` until LangGraph raises:

- `GraphRecursionError`
- `Recursion limit of 25 reached without hitting a stop condition`

If you are using an LLM-driven agent loop, this usually means your model output is not producing a terminal action like `finish`, `final`, or `stop`.

## Other Possible Causes

### 1. Your conditional router returns an invalid or unstable target

If your router sometimes returns `"agent"` and sometimes returns `"Agent"` or `undefined`, you can get unexpected looping behavior.

```ts
// Broken
graph.addConditionalEdges("router", (state) => {
  if (!state.messages.length) return "Agent"; // wrong case
  return undefined as any; // unstable
});

// Fixed
graph.addConditionalEdges("router", (state) => {
  if (!state.messages.length) return "start";
  return END;
});

Keep node names exact. LangGraph does not normalize names for you.

2. Your reducer keeps old state alive

A bad reducer can preserve stale values so your termination check never flips.

// Broken reducer keeps appending forever
channels: {
  stepCount: {
    value: (left = 0, right = 0) => left + right,
    default: () => 0,
  },
}

If every node returns { stepCount: 1 }, this is fine. If your node never increments correctly, the condition may never change.

Use explicit counters:

stepCount: {
  value: (_left = 0, right = 0) => right,
  default: () => 0,
}

3. A tool node returns control to the agent without changing anything

This is common with ReAct-style loops. The model calls a tool, but the tool result does not affect the next decision.

// Broken flow
agent -> tools -> agent -> tools -> agent

// Fix by adding a stop condition in state
graph.addConditionalEdges("tools", (state) =>
  state.toolCalls >= 3 ? END : "agent"
);

If you are using ToolNode, make sure the assistant message actually contains tool calls and that your post-tool routing checks for completion.

4. Your entrypoint cycles back through multiple nodes

Sometimes the loop is not obvious because it spans several nodes.

// Broken cycle
start -> classify -> enrich -> classify -> enrich

// Fix with terminal branch
classify -> enrich -> END

This happens when each node assumes another one will eventually terminate. In production graphs, every cycle needs one explicit exit path.

How to Debug It

  1. Lower the recursion limit and inspect the failing path

    const result = await app.invoke(input, { recursionLimit: 5 });
    

    A smaller limit makes the loop fail faster while you debug.

  2. Log every node transition Add logs inside each node and router:

    graph.addNode("agent", async (state) => {
      console.log("agent state:", state);
      return { ... };
    });
    

    If you see the same state over and over, your termination logic is broken.

  3. Check what your conditional edge returns Print the router output before returning it.

    • Must match an existing node name exactly
    • Must return END when finished
    • Must not return undefined
  4. Inspect reducers and state updates If a field drives branching, confirm it actually changes.

    • Counter increments?
    • Boolean flips?
    • Tool results stored?
    • Message history appended correctly?

Prevention

  • Always define one explicit terminal path per cycle.
  • Use a step counter or max-iterations guard in agent loops.
  • Write tests for router outputs with both normal and empty-state inputs.
  • Treat recursionLimit as a safety net, not as flow control.

If you want a stable LangGraph agent in TypeScript, design for termination first. The graph should be able to prove it can stop before you let it call tools or route between nodes.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides