How to Fix 'intermittent 500 errors' in LangGraph (TypeScript)
Intermittent 500s in LangGraph usually mean your graph is throwing an exception somewhere inside a node, reducer, tool call, or streaming handler, and the failure is not being surfaced cleanly at the edge. In TypeScript projects, this often shows up when the same graph works for some inputs, then fails on specific state shapes, async timing, or message payloads.
The annoying part is that the HTTP 500 is just the symptom. The real bug is usually one of a few predictable patterns: invalid state updates, non-serializable values in state, uncaught async errors, or a mismatch between your graph schema and what a node returns.
The Most Common Cause
The #1 cause I see is returning an invalid state update from a node. In LangGraph, every node must return a partial state object that matches the graph’s declared state shape. If you return the wrong key, mutate state in place, or send back something non-serializable, you can get errors like:
- •
InvalidUpdateError: Expected node to return a valid update - •
TypeError: Converting circular structure to JSON - •
500 Internal Server Errorfrom your API wrapper
Here’s the broken pattern:
| Broken | Fixed |
|---|---|
| ```ts | |
| import { StateGraph } from "@langchain/langgraph"; |
type State = { messages: { role: string; content: string }[]; answer?: string; };
const graph = new StateGraph<State>()
.addNode("generate", async (state) => {
state.answer = "Hello"; // mutation
return { result: "Hello" }; // wrong key
})
.addEdge("start", "generate")
.addEdge("generate", "end")
.compile();
|ts
import { StateGraph } from "@langchain/langgraph";
type State = { messages: { role: string; content: string }[]; answer?: string; };
const graph = new StateGraph<State>() .addNode("generate", async (_state) => { return { answer: "Hello" }; // valid partial update }) .addEdge("start", "generate") .addEdge("generate", "end") .compile();
If you’re using reducers, make sure the field is defined with a merge strategy and that your node returns the expected shape. For example:
```ts
import { Annotation, StateGraph } from "@langchain/langgraph";
const GraphState = Annotation.Root({
messages: Annotation<string[]>({
reducer: (left, right) => left.concat(right),
default: () => [],
}),
});
const graph = new StateGraph(GraphState)
.addNode("append", async () => ({
messages: ["new message"],
}))
.addEdge("__start__", "append")
.addEdge("append", "__end__")
.compile();
If you return { messages: "new message" } instead of an array, the reducer will blow up later and look like an intermittent server failure.
Other Possible Causes
1) Uncaught exceptions inside async nodes
If a node throws and you don’t catch it, the error bubbles out as a generic runtime failure.
.addNode("lookup", async (state) => {
const res = await fetch(state.url);
if (!res.ok) throw new Error(`Upstream failed: ${res.status}`);
return { data: await res.json() };
})
Fix by wrapping risky code and returning controlled failures if your flow expects them:
.addNode("lookup", async (state) => {
try {
const res = await fetch(state.url);
if (!res.ok) return { error: `Upstream failed: ${res.status}` };
return { data: await res.json() };
} catch (e) {
return { error: e instanceof Error ? e.message : "Unknown error" };
}
})
2) Non-serializable values in state
Putting Response, Request, class instances, or circular objects into LangGraph state can trigger JSON serialization problems when streaming or persisting.
// bad
return { response }; // Response object
Use plain JSON instead:
const text = await response.text();
return { status: response.status, body: text };
3) Mismatch between schema and returned payload
If your TypeScript types say one thing but your runtime returns another, LangGraph won’t save you.
type State = {
count: number;
};
.addNode("inc", async () => ({
count: "1" as unknown as number,
}))
This may compile if you force-cast it, then fail downstream in reducers or conditionals. Keep runtime values aligned with schema.
4) Tool calls failing only on certain inputs
A tool node may work for common cases but fail on edge-case user input.
.addNode("tool", async (state) => {
const id = Number(state.userId);
if (Number.isNaN(id)) throw new Error("Invalid userId");
});
This often looks intermittent because only some requests hit bad input. Validate before calling tools.
How to Debug It
- •
Run the graph node-by-node
- •Execute each node with a known input.
- •Confirm what each node returns.
- •Look for the first node that returns an unexpected shape.
- •
Log raw state before and after every node
- •Print
JSON.stringify(state)if possible. - •Check for
undefined, circular objects, class instances, or wrong keys. - •If serialization fails here, that’s your bug.
- •Print
- •
Turn intermittent failures into deterministic ones
- •Re-run the exact failing payload.
- •Save request bodies that trigger
500 Internal Server Error. - •If using tools or LLM calls, stub them temporarily to isolate graph logic.
- •
Inspect stack traces from LangGraph internals
- •Search for classes like
InvalidUpdateError,EmptyChannelError, or reducer-related exceptions. - •If you see an error only at the API layer but not in your app logs, add logging inside each
.addNode()handler.
- •Search for classes like
Prevention
- •
Keep LangGraph state plain and serializable:
- •strings, numbers, booleans, arrays, objects
- •no class instances, no
Response, no circular references
- •
Make every node return one valid partial update:
- •match the declared schema exactly
- •don’t mutate incoming state in place
- •
Add input validation at graph boundaries:
- •validate IDs, payloads, and tool arguments before entering nodes
- •fail early with explicit errors instead of letting them become opaque 500s
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