Debugging a compiled graph
Why this matters
When your graph produces unexpected output, you need to see what's actually happening inside each node and state change: guessing is slow and builds bad mental models.
Explanation
Debugging a compiled graph means inspecting the execution flow of a StateGraph after you call .compile().invoke(). You can see which nodes ran, what state they received, and what they returned. The langgraph runtime provides debug output through print statements in your nodes and by inspecting the intermediate values returned from invoke(). Mechanically, when you call graph.invoke(input_state), the graph executes sequentially through nodes, and each node can print debug information. You can also capture the entire execution history by accessing the return value and tracing what changed at each step. When to use: Always add minimal debug output to nodes during development, especially when adding conditional edges or complex state transformations.
Analogy
Debugging a graph is like adding checkpoints along a hiking trail: you mark where the hiker is, what they're carrying, and where they're heading next. Without checkpoints, you can't tell if they took a wrong turn.
Code
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class State(TypedDict):
user_input: str
uppercase: str
length: int
def node_uppercase(state: State) -> State:
print(f"[DEBUG] node_uppercase received: {state}")
state["uppercase"] = state["user_input"].upper()
print(f"[DEBUG] node_uppercase produced uppercase: {state['uppercase']}")
return state
def node_length(state: State) -> State:
print(f"[DEBUG] node_length received: {state}")
state["length"] = len(state["uppercase"])
print(f"[DEBUG] node_length produced length: {state['length']}")
return state
graph = StateGraph(State)
graph.add_node("uppercase", node_uppercase)
graph.add_node("length", node_length)
graph.add_edge(START, "uppercase")
graph.add_edge("uppercase", "length")
graph.add_edge("length", END)
compiled = graph.compile()
result = compiled.invoke({"user_input": "hello"})
print(f"\n[FINAL] Result: {result}") [DEBUG] node_uppercase received: {'user_input': 'hello', 'uppercase': '', 'length': 0}
[DEBUG] node_uppercase produced uppercase: HELLO
[DEBUG] node_length received: {'user_input': 'hello', 'uppercase': 'HELLO', 'length': 0}
[DEBUG] node_length produced length: 5
[FINAL] Result: {'user_input': 'hello', 'uppercase': 'HELLO', 'length': 5} What just happened?
The compiled graph executed two nodes in sequence: <code>node_uppercase</code> transformed the input and printed debug info showing the before/after state, then <code>node_length</code> ran with the updated state and did the same. The final <code>invoke()</code> returned the complete state after all nodes finished, showing us the full transformation chain.
Common gotcha
Debug output from print() inside nodes goes to stdout, but it gets mixed with other output. If your graph has many nodes or runs in async mode, timestamps become critical for matching which print came from which execution. Don't rely on print order alone in production: use proper logging with node IDs.
Error recovery
AttributeError: 'dict' object has no attributeKeyError when accessing stateNo debug output at allExperienced dev note
The single best debugging habit: print the entire state dict at the start of every node during development, not just the fields you think matter. You'll catch state-key typos and missing initialization in seconds instead of spending 20 minutes staring at logic that's actually correct. Remove these prints once the graph runs correctly: they're scaffolding, not permanent instrumentation. For production debugging, switch to structured logging with node names and trace IDs; it scales better than print statements.
Check your understanding
You have a three-node graph where the second node is failing. You see debug output from node 1 and node 3, but nothing from node 2. What is the most likely cause, and how would you confirm it?
Show answer hint
A correct answer identifies that missing debug output means the node never executed (not that it executed silently), which points to the edge definition. The confirmation would be inspecting the graph topology or adding a print before the state mutation in node 2.
langgraph.graph, not legacy MessageGraph.