High severity intermediate · Fix: 5-15 min

ValueError: END node unreachable

langgraph.graph.state_graph.ValueError: END node unreachable from conditional edge

What this error means
LangGraph's StateGraph raised an error because a conditional edge routing function never returns a valid node path that leads to the END node, leaving the graph in an unreachable dead-end state.

Stack trace

traceback
Traceback (most recent call last):
  File "your_script.py", line 45, in <module>
    graph.compile()
  File "langgraph/graph/state_graph.py", line 342, in compile
    self._validate_graph()
  File "langgraph/graph/state_graph.py", line 198, in _validate_graph
    raise ValueError(f"END node unreachable from conditional edge '{edge_name}'")
ValueError: END node unreachable from conditional edge 'route_decision'
QUICK FIX
Add graph.add_edge(node_name, END) after every conditional node, and ensure your routing function has a default return that reaches END: def route(state): ... ; return 'next_node' if condition else END.

Why it happens

LangGraph's StateGraph validates that every conditional edge has at least one code path that eventually leads to the END node. If a conditional function returns a node name that loops back on itself, or routes to a node with no outgoing edges to END, or references a non-existent node, the graph becomes deadlocked. LangGraph detects this at compile time and raises this error to prevent runtime hangs.

Detection

Enable debug logging before calling graph.compile(): set logging.getLogger('langgraph').setLevel(logging.DEBUG). Review your conditional routing function's return values and trace them manually through the graph structure. Use graph.get_state().values to inspect which nodes have incoming/outgoing edges during development.

Causes & fixes

1

Conditional routing function returns a node name that loops back to itself without progressing toward END

✓ Fix

Ensure your conditional function never returns the current node's name as the next step. Trace the return values: if state['step'] == 'validate' and validation fails, return 'retry_process' not 'validate'. Always have a path that eventually leads to END.

2

Conditional function routes to a node that has no outgoing edges to END or other progress nodes

✓ Fix

Add explicit edges from every intermediate node. Use graph.add_edge(node_name, END) or graph.add_conditional_edges(node_name, routing_func, {END: END}) to ensure all dead-end nodes explicitly connect to END.

3

Conditional function returns a node name that doesn't exist in the graph

✓ Fix

Verify all node names in your routing function return values match exactly with nodes added via graph.add_node(). Python is case-sensitive: 'Process' ≠ 'process'. Use constants: PROCESS_NODE = 'process'; return PROCESS_NODE to avoid typos.

4

All branches in the conditional function return non-END nodes, with no default fallback to END

✓ Fix

Add a catch-all condition that returns END: def route(state): if state.get('retry_count', 0) >= 3: return END; if error: return 'handle_error'; return 'retry'. Always include a default that prevents infinite loops.

Code: broken vs fixed

Broken - triggers the error
python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import Literal
from pydantic import BaseModel
import os

class AgentState(BaseModel):
    query: str
    attempts: int = 0
    result: str = ""

def process_query(state: AgentState):
    """Process the query."""
    return {"result": f"Processed: {state.query}", "attempts": state.attempts + 1}

def check_result(state: AgentState) -> Literal["process_query", "retry", END]:
    """Route based on result — BUG: 'retry' node doesn't exist and has no path to END."""
    if "error" in state.result.lower():
        return "retry"  # This node was never added to the graph!
    return END

# Build graph with missing node
graph_builder = StateGraph(AgentState)
graph_builder.add_node("process_query", process_query)
graph_builder.add_node("check_result", check_result)
graph_builder.set_entry_point("process_query")
graph_builder.add_edge("process_query", "check_result")
graph_builder.add_conditional_edges(
    "check_result",
    check_result,
    {"process_query": "process_query", "retry": "retry", END: END}  # 'retry' doesn't exist!
)

# This line raises: ValueError: END node unreachable from conditional edge 'check_result'
graph = graph_builder.compile()
Fixed - works correctly
python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import Literal
from pydantic import BaseModel
import os

class AgentState(BaseModel):
    query: str
    attempts: int = 0
    result: str = ""
    max_retries: int = 3

def process_query(state: AgentState):
    """Process the query."""
    return {"result": f"Processed: {state.query}", "attempts": state.attempts + 1}

def retry_handler(state: AgentState):
    """Handle retry logic."""
    return {"result": f"Retry attempt {state.attempts}"}

def check_result(state: AgentState) -> Literal["retry_handler", END]:
    """Route based on result — FIXED: routes to an actual node and has fallback to END."""
    if "error" in state.result.lower() and state.attempts < state.max_retries:
        return "retry_handler"  # This node now exists
    return END  # Fallback path always reaches END

# Build graph with all nodes and edges properly defined
graph_builder = StateGraph(AgentState)
graph_builder.add_node("process_query", process_query)
graph_builder.add_node("retry_handler", retry_handler)  # FIXED: added missing node
graph_builder.add_node("check_result", check_result)

graph_builder.set_entry_point("process_query")
graph_builder.add_edge("process_query", "check_result")

# FIXED: conditional edges route to nodes that exist, with fallback to END
graph_builder.add_conditional_edges(
    "check_result",
    check_result,
    {"retry_handler": "retry_handler", END: END}
)

# FIXED: explicitly add edge from retry node back to processing
graph_builder.add_edge("retry_handler", "process_query")

memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)  # Compiles successfully

# Test the graph
state = AgentState(query="test query", max_retries=3)
result = graph.invoke({"query": "test query"})
print(f"✓ Graph executed successfully: {result}")
Added the missing 'retry_handler' node, ensured the conditional routing function returns only node names that exist in the graph, and guaranteed every path leads to END via explicit edge definitions and a default END return in the routing function.

Workaround

If you cannot immediately refactor your graph structure, replace add_conditional_edges() with a manual state machine using if/elif in a single processing node: def unified_processor(state): if state['needs_retry']: state['node_type'] = 'retry'; return state; elif state['done']: state['node_type'] = 'end'; return state. This bypasses conditional edges entirely until you can restructure the graph.

Prevention

Build graphs with explicit node lists upfront and validate them: allowed_nodes = {'process', 'validate', 'retry', END}; assert all(node in allowed_nodes for node in routing_results), 'Invalid node in routing function'. Use TypedDict or Enum for node names to catch typos at type-check time: from enum import Enum; class Nodes(str, Enum): PROCESS='process'; RETRY='retry'. Always include a default case in routing functions that returns END. Test graph.compile() early during development, not after adding multiple nodes.

Python 3.10+ · langgraph >=0.1.0 · tested on 0.2.x
Verified 2026-04
Verify ↗

Community Notes

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