KeyError in state: field not in TypedDict
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.
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
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}") 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
KeyErrorTypeError when invoking graphExperienced 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.