How to Fix 'state not updating during development' in LangChain (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
state-not-updating-during-developmentlangchaintypescript

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 patternFixed pattern
Mutates existing state objectReturns a new object/array
Works in plain logs sometimesBreaks with stale UI/dev state
Hard to notice in agent loopsPredictable 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

  1. 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.
  2. 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.
  3. Inspect async boundaries

    • Any await between reading and writing state is suspicious.
    • Add logs before and after the await to see if another call updated the value first.
  4. 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.

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

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