Code Beginner easy · 4 min

KeyError in state: field not in TypedDict

What you will learn
StateGraph enforces a strict contract: you can only access state fields you declared in your TypedDict, or you'll hit a KeyError at runtime.

Why this matters

This is the #1 runtime error beginners hit in langgraph: it happens silently in development then breaks in production because a node tried to read a field that doesn't exist in the state schema. Understanding the contract prevents hours of debugging.

Skip if: You don't need to worry about this if you're using plain dictionaries instead of TypedDict (but you should use TypedDict: it catches these errors before runtime).

Explanation

What it is: langgraph's StateGraph requires you to define a TypedDict that declares every field your graph will use. If a node tries to access a field not in that TypedDict, you get a KeyError. This is by design: it's a safety feature, not a bug.

How it works: When you call StateGraph(MyState), you're telling langgraph "this graph uses exactly these fields and types." The state object is validated against this schema before and after every node runs. If a node tries to read state['field_that_doesnt_exist'], Python raises KeyError immediately. The TypedDict acts as a contract: declare all fields upfront, or don't access them.

When to use it: Always use TypedDict with StateGraph. It's not optional: it's the foundation of type safety in langgraph. Even if you think you don't need it, you do: it catches typos, prevents fields from being added mid-execution, and documents your graph's data flow for teammates.

Analogy

Think of TypedDict as a database schema. You can't query a column that doesn't exist in your table: the database will reject it. TypedDict does the same for your graph state. You declare the columns first, then you can use them. Trying to access a field you didn't declare is like querying a non-existent column.

Code

python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    user_input: str
    processed: bool

def node_a(state: State) -> State:
    print(f"Processing: {state['user_input']}")
    return {"processed": True}

def node_b(state: State) -> State:
    try:
        print(f"Accessing undefined field: {state['nonexistent']}")
    except KeyError as e:
        print(f"KeyError caught: {e}")
        print(f"Available fields: {list(state.keys())}")
    return state

graph = StateGraph(State)
graph.add_node("node_a", node_a)
graph.add_node("node_b", node_b)
graph.add_edge(START, "node_a")
graph.add_edge("node_a", "node_b")
graph.add_edge("node_b", END)

compiled = graph.compile()
result = compiled.invoke({"user_input": "hello world", "processed": False})
print(f"\nFinal state: {result}")
Output
Processing: hello world
KeyError caught: 'nonexistent'
Available fields: dict_keys(['user_input', 'processed'])

Final state: {'user_input': 'hello world', 'processed': True}

What just happened?

The code defined a TypedDict with two fields: <code>user_input</code> and <code>processed</code>. Node A ran successfully because it accessed only <code>user_input</code> (which exists in the schema). Node B tried to access <code>state['nonexistent']</code>, which is not in the TypedDict, triggering a KeyError. The except block caught it, printed the error, and showed which fields actually exist. The graph continued and completed successfully because the error was handled.

Common gotcha

Developers often assume they can add new fields dynamically mid-execution: like state['new_field'] = value. In standard StateGraph, this doesn't work if new_field isn't in your TypedDict. You must declare all fields upfront. If you genuinely need dynamic fields, you use a dict field inside your TypedDict to hold variable keys, but the dict itself must be declared.

Error recovery

KeyError
The field name in the error message is not in your TypedDict. Check the spelling, then add it to your TypedDict class with the correct type annotation (e.g., <code>class State(TypedDict): new_field: str</code>).
TypeError when invoking graph
You passed a dict to <code>invoke()</code> with a key that doesn't match your TypedDict. Remove the extra key or add it to the TypedDict definition.

Experienced dev note

TypedDict is not just validation: it's documentation. A year from now, a teammate (or you) will read your TypedDict and instantly understand what data flows through the graph. More importantly: use typing.get_type_hints(State) if you need to inspect the schema programmatically. This saves debugging time when state gets complex. Also: if you're returning state updates from a node, return a dict with only the fields you changed, not the whole state: langgraph merges it for you. Returning fields that aren't in your TypedDict will silently fail or raise an error depending on how strict you've configured validation.

Check your understanding

If you add a new field to your TypedDict after your graph is already compiled, will existing nodes that don't use that field break? What about nodes that do use it? Explain what happens in each case.

Show answer hint

The answer must distinguish between: (1) compilation time vs. runtime, (2) fields that are optional vs. required in TypedDict, and (3) whether omitting a field from a node's return dict is the same as trying to read a non-existent field.

VERSION This behavior is consistent in langgraph 0.2.x. In langgraph < 0.2.0, MessageGraph used a different pattern; StateGraph has always enforced TypedDict contracts.
NEXT

Next, learn how to handle optional fields in state using <code>total=False</code> in TypedDict, so you can make state updates conditional without triggering KeyErrors.

Community Notes

No notes yetBe the first to share a version-specific fix or tip.