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

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

If you’re seeing state not updating during development in LangGraph TypeScript, it usually means your graph is running, but the state you think you’re mutating is not actually being persisted between steps. In practice, this shows up during local dev when a node returns the wrong shape, a reducer is missing, or you’re reusing stale compiled graph code after edits.

The error often appears as a runtime mismatch: the graph executes, but downstream nodes still receive the old state. In LangGraph terms, your StateGraph is fine structurally, but your updates are either not being merged or are being overwritten.

The Most Common Cause

The #1 cause is returning a partial update without defining the state schema and reducers correctly.

In LangGraph TypeScript, state updates are merged based on the schema. If you expect an array to append or an object to merge, but you defined it as a plain field with no reducer, your “update” will replace the value or be ignored in ways that look like state isn’t updating.

Broken vs fixed

Broken patternFixed pattern
Returns state mutation assumptions without reducersUses Annotation.Root with explicit reducers
Updates arrays by assignmentAppends with a reducer
Reads stale values in later nodesReceives merged state correctly
// BROKEN
import { StateGraph } from "@langchain/langgraph";

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

const graph = new StateGraph<GraphState>({
  channels: {
    messages: {
      value: (x) => x,
      default: () => [],
    },
  },
});

graph.addNode("addMessage", async (state) => {
  return {
    messages: [...state.messages, "hello"],
  };
});
// FIXED
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);

graph.addNode("addMessage", async () => {
  return {
    messages: ["hello"],
  };
});

The important part is this: LangGraph expects state updates to be expressed through the schema’s merge behavior. If you want accumulation, define accumulation. If you want replacement, define replacement explicitly.

Other Possible Causes

1. You’re returning the wrong key from a node

If your node returns a field that does not exist in the schema, LangGraph won’t merge it into state the way you expect.

// Wrong
return { message: "hello" }; // schema expects `messages`
// Right
return { messages: ["hello"] };

This is common when renaming fields during refactors. The graph compiles, but downstream nodes see no change.

2. You mutated nested objects in place

LangGraph works best with immutable updates. Mutating nested objects can make debugging confusing because logs show changed references locally, but graph transitions may not reflect them cleanly.

// Wrong
state.profile.name = "new name";
return { profile: state.profile };
// Right
return {
  profile: {
    ...state.profile,
    name: "new name",
  },
};

If you are using checkpointing or persistence later, immutable updates matter even more.

3. Your compiled graph is stale during development

This happens a lot with ts-node, watch mode, or hot reload setups. You edit the node logic, but your process keeps running an older compiled artifact.

{
  "scripts": {
    "dev": "tsx watch src/index.ts"
  }
}

If you build to dist/, make sure you are not accidentally running:

node dist/index.js

while editing src/index.ts. That mismatch produces “it should work” symptoms that look like state bugs.

4. Your checkpointer or thread config is wrong

If you use persistence and forget to pass a stable thread ID, LangGraph may treat each run as a fresh conversation.

const result = await app.invoke(
  { messages: ["hi"] },
  {
    configurable: {
      thread_id: "user-123",
    },
  }
);

Without a stable thread_id, your state can appear to reset on every request.

Also verify that your checkpointer is actually wired:

import { MemorySaver } from "@langchain/langgraph";

const checkpointer = new MemorySaver();
const app = graph.compile({ checkpointer });

How to Debug It

  1. Print the exact output of each node

    • Log what each node returns.
    • Confirm it matches the schema keys exactly.
    • Example:
      graph.addNode("debugNode", async (state) => {
        const update = { messages: ["test"] };
        console.log("node output:", update);
        return update;
      });
      
  2. Inspect the compiled graph input/output

    • Run one node at a time if possible.
    • Check whether the issue starts at node output or after merging.
    • If node output is correct but downstream state is wrong, it’s usually reducer/schema related.
  3. Verify your annotations and reducers

    • Look for fields that should accumulate:
      • arrays of messages
      • event history
      • tool traces
    • Make sure those fields have reducers.
    • A plain field assignment will overwrite rather than append.
  4. Check persistence configuration

    • If using MemorySaver, confirm:
      • compile({ checkpointer }) is present
      • thread_id is stable across calls
    • If using Redis/Postgres/etc., confirm the same session key is reused.

Prevention

  • Define every mutable field in Annotation.Root with an explicit reducer.
  • Keep node outputs immutable and return only patch-like updates.
  • Add a small integration test that runs two consecutive invocations and asserts state carries forward.

A good regression test catches this class of bug early:

const first = await app.invoke({ messages: ["one"] }, { configurable: { thread_id: "t1" } });
const second = await app.invoke({ messages: ["two"] }, { configurable: { thread_id: "t1" } });

expect(second.messages).toContain("one");
expect(second.messages).toContain("two");

If your “state not updating during development” issue persists after fixing reducers and thread config, the problem is usually not LangGraph itself. It’s almost always one of these three things:

  • schema mismatch
  • stale build output
  • missing persistence config

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