How to Fix 'chain execution stuck' in LangGraph (TypeScript)
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 pattern | Fixed pattern |
|---|---|
| Node mutates local data and returns nothing | Node returns a valid partial state update |
| Conditional edge has no terminal path | Conditional edge routes to END |
| State key never changes | State 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
ENDwhen 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
- •
Log every node input and output
- •Print the full state at entry and exit.
- •If a node returns
undefined, that’s your first bug.
- •
Check terminal routing
- •Confirm at least one branch reaches
END. - •If you use
addConditionalEdges, verify all possible outputs are mapped.
- •Confirm at least one branch reaches
- •
Run nodes in isolation
- •Call each node directly with sample state.
- •Verify it returns a plain object with updated fields.
- •
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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