How to Fix 'chain execution stuck' in LangGraph (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
chain-execution-stucklanggraphtypescript

If you’re seeing chain execution stuck in LangGraph, it usually means your graph never reached a valid terminal state or one of your nodes stopped making progress. In TypeScript, this most often shows up when a node returns the wrong shape, forgets to update state, or creates a loop with no exit condition.

The error is usually not in LangGraph itself. It’s almost always in the graph logic: conditional routing, state merging, or an async node that never resolves.

The Most Common Cause

The #1 cause is a node that returns nothing useful to the graph, so the workflow keeps waiting for state changes that never happen.

In LangGraph, every node should return a partial state update. If you mutate local variables and return undefined, or return an object that doesn’t match the expected state schema, execution can stall.

Broken vs fixed

Broken patternFixed pattern
Node mutates local data and returns nothingNode returns a valid partial state update
Conditional edge has no terminal pathConditional edge routes to END
State key never changesState key is updated on each pass
import { StateGraph, END } from "@langchain/langgraph";

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

const badNode = async (state: GraphState) => {
  state.messages.push("processed");
  // ❌ returns nothing meaningful
};

const goodNode = async (state: GraphState): Promise<Partial<GraphState>> => {
  return {
    messages: [...state.messages, "processed"],
    done: true,
  };
};

const workflow = new StateGraph<GraphState>()
  .addNode("badNode", badNode)
  .addNode("goodNode", goodNode)
  .addEdge("__start__", "badNode")
  .addEdge("badNode", END);

The broken version can trigger behavior that looks like:

  • Error: chain execution stuck
  • LangGraphError: Graph did not reach END
  • InvalidUpdateError: Node returned undefined

The fix is simple:

  • return a partial state object
  • make sure at least one field changes
  • route to END when work is complete

Other Possible Causes

1. Conditional edges never hit END

If your router only returns internal nodes, the graph can loop forever.

const router = (state: GraphState) => {
  if (state.done) return END;
  return "badNode";
};

Broken version:

.addConditionalEdges("router", router, {
  badNode: "badNode",
});

Fixed version:

.addConditionalEdges("router", router, {
  badNode: "badNode",
  [END]: END,
});

2. Async node hangs forever

A promise that never resolves will freeze the run and look like a stuck chain.

const hangingNode = async () => {
  await new Promise(() => {
    // ❌ never resolves
  });
};

Fix it with timeouts around external calls:

const timeout = (ms: number) =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("timeout")), ms)
  );

await Promise.race([
  callModel(),
  timeout(10_000),
]);

3. State schema mismatch

If your node returns keys not declared in the graph state, updates may be ignored or rejected depending on setup.

type GraphState = {
  messages: string[];
};

const wrong = async (): Promise<Partial<GraphState>> => {
  return {
    output: "done", // ❌ not part of GraphState
  } as any;
};

Fix by keeping the returned shape aligned with the schema:

const right = async (): Promise<Partial<GraphState>> => {
  return {
    messages: ["done"],
  };
};

4. Reducer/merge logic prevents progress

If your reducer always preserves old values and never lets the graph advance, conditional routing may keep selecting the same node.

type GraphState = {
  step: number;
};

const incrementStep = async (state: GraphState): Promise<Partial<GraphState>> => ({
  step: state.step + 1,
});

Broken example:

const stuckStep = async (state: GraphState): Promise<Partial<GraphState>> => ({
  step: state.step, // ❌ no change
});

If your router depends on step, this creates an infinite repeat.

How to Debug It

  1. Log every node input and output

    • Print the full state at entry and exit.
    • If a node returns undefined, that’s your first bug.
  2. Check terminal routing

    • Confirm at least one branch reaches END.
    • If you use addConditionalEdges, verify all possible outputs are mapped.
  3. Run nodes in isolation

    • Call each node directly with sample state.
    • Verify it returns a plain object with updated fields.
  4. Add hard timeouts around external calls

    • Wrap LLM calls, tool calls, database queries, and HTTP requests.
    • If a timeout fires, the issue is not LangGraph; it’s the dependency inside the node.

A practical debug pattern:

const tracedNode = async (state: GraphState): Promise<Partial<GraphState>> => {
  console.log("IN:", JSON.stringify(state));

  const next = await goodNode(state);

  console.log("OUT:", JSON.stringify(next));
  return next;
};

If you see repeated identical input/output pairs across multiple steps, your graph is looping without progress.

Prevention

  • Always make every node return a partial state update.
  • Make terminal conditions explicit and test them before shipping.
  • Add tracing early:
    • log state transitions
    • inspect conditional routing
    • enforce timeouts on all external I/O

If you want one rule to remember: a LangGraph run gets stuck when no node produces a meaningful state change or no path reaches END. Fix those two things first before digging into anything more exotic.


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