How to Fix 'state not updating during development' in LangChain (TypeScript)
If you’re seeing state not updating during development in a LangChain TypeScript app, the issue is usually not LangChain “forgetting” state. It’s almost always one of three things: you’re mutating state in place, you’re reading stale values from a closure, or your dev environment is hot-reloading code in a way that resets module state.
This shows up most often when building agent loops, tool runners, or chat session stores with RunnableSequence, AgentExecutor, or custom memory/state containers.
The Most Common Cause — Mutating State In Place
The #1 cause is this: you update an object or array without creating a new reference, then expect your framework or runtime to notice the change.
That’s a classic bug when LangChain state is wrapped by React state, Zustand, Redux, or your own event loop. The LangChain part is usually fine; the outer state container never sees a new object.
Broken vs fixed
| Broken pattern | Fixed pattern |
|---|---|
| Mutates existing state object | Returns a new object/array |
| Works in plain logs sometimes | Breaks with stale UI/dev state |
| Hard to notice in agent loops | Predictable and debuggable |
// BROKEN
type AgentState = {
messages: string[];
step: number;
};
let state: AgentState = {
messages: [],
step: 0,
};
function addMessage(message: string) {
// In-place mutation
state.messages.push(message);
state.step += 1;
}
addMessage("Hello");
console.log(state.step); // 1
// FIXED
type AgentState = {
messages: string[];
step: number;
};
let state: AgentState = {
messages: [],
step: 0,
};
function addMessage(message: string) {
// New references
state = {
...state,
messages: [...state.messages, message],
step: state.step + 1,
};
}
addMessage("Hello");
console.log(state.step); // 1
If you’re using LangChain message objects directly, the same rule applies:
import { HumanMessage } from "@langchain/core/messages";
let messages = [new HumanMessage("Hi")];
// Broken
messages.push(new HumanMessage("Next")); // mutates in place
// Fixed
messages = [...messages, new HumanMessage("Next")];
Other Possible Causes
1. Stale closure inside async agent code
This happens when your callback captures old state and keeps using it after an await.
let count = 0;
async function run() {
const current = count;
await Promise.resolve();
count = current + 1; // stale value if count changed elsewhere
}
Fix by reading the latest value at update time, not before async work:
async function run() {
await Promise.resolve();
count = count + 1;
}
In LangChain agent loops, this often appears inside handleToolCall, custom callbacks, or streaming handlers.
2. Dev server hot reload resetting module scope
If you store LangChain session data in a top-level variable, Vite/Next.js/Nodemon can reload the module and wipe it.
// BROKEN
let sessionState = new Map<string, string[]>();
On refresh or file save, that module can be re-evaluated and your map is recreated.
Use an external store instead:
// FIXED
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);
Or at minimum attach it to a singleton during development:
const globalForState = globalThis as typeof globalThis & {
sessionState?: Map<string, string[]>;
};
export const sessionState =
globalForState.sessionState ?? new Map<string, string[]>();
if (process.env.NODE_ENV !== "production") {
globalForState.sessionState = sessionState;
}
3. Mixing mutable objects with LangChain message history
If you use RunnableWithMessageHistory, make sure your history implementation returns stable values and appends correctly.
Bad implementation:
import { BaseChatMessageHistory } from "@langchain/core/chat_history";
class BadHistory extends BaseChatMessageHistory {
messages: any[] = [];
async addMessage(message: any) {
this.messages.push(message); // mutable only; easy to break with shared refs
}
async clear() {
this.messages.length = 0;
}
}
Better approach:
class GoodHistory extends BaseChatMessageHistory {
messages: any[] = [];
async addMessage(message: any) {
this.messages = [...this.messages, message];
}
async clear() {
this.messages = [];
}
}
If multiple requests share the same instance, mutable arrays will bite you fast.
4. Incorrect use of RunnableConfig / missing thread identifiers
When using graph-like patterns or persisted runs, forgetting to pass stable config can make it look like state is not updating.
const result = await chain.invoke(input);
// no session/thread id passed
If your setup depends on per-session tracking, pass identifiers consistently:
const result = await chain.invoke(input, {
configurable: {
thread_id: "user-123",
session_id: "chat-456",
},
});
Without stable identifiers, each invocation can behave like a fresh run.
How to Debug It
- •
Check whether the value changes by reference
- •Log both the object and its identity path.
- •If you mutate arrays with
.push()or objects with direct assignment on shared references, fix that first.
- •
Search for top-level mutable variables
- •Look for
let cache = ...,const history = [], or singleton maps in module scope. - •If those are holding chat/session data in dev mode, expect resets on hot reload.
- •Look for
- •
Inspect async boundaries
- •Any
awaitbetween reading and writing state is suspicious. - •Add logs before and after the await to see if another call updated the value first.
- •Any
- •
Verify your LangChain config
- •If you use
RunnableWithMessageHistory,AgentExecutor, or graph persistence patterns, confirm session IDs are stable. - •If a run appears stateless every time, your config is probably missing the key that ties updates together.
- •If you use
Prevention
- •Treat all agent/session updates as immutable.
- •Keep request-scoped state out of module scope; use Redis, Postgres, or another external store for anything that must survive reloads.
- •Pass explicit IDs through LangChain config for every conversation or workflow run.
If you want one rule to remember: when state “doesn’t update,” assume reference mutation or stale scope before blaming LangChain itself. That’s where this error usually lives.
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