Command(goto=...) does not override explicit edges from add_edge() #1088

Closed
opened 2026-02-20 17:43:01 -05:00 by yindo · 1 comment
Owner

Originally created by @Juussticee on GitHub (Dec 10, 2025).

Originally assigned to: @sydney-runkle on GitHub.

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

import functools
import operator
import asyncio
from typing import TypedDict, List, Callable, Annotated

from langgraph.errors import GraphInterrupt
from langgraph.graph import StateGraph, END
from langgraph.types import Command

def error_handler():
    def decorator(func: Callable):
        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs):
            try:
                print(f"Starting execution: {func.__name__}")
                result = await func(*args, **kwargs)
                print(f"Successfully completed: {func.__name__}")
                return result
            except GraphInterrupt:
                raise
            except Exception as e:
                print(f"Error in {func.__name__}: {str(e)}")
                # Attempt to redirect to exception node
                return Command(goto="exception_node")
        return async_wrapper
    return decorator

class GraphState(TypedDict):
    messages: Annotated[List[str], operator.add]

@error_handler()
async def node_a(state: GraphState):
    print("---Executing Node A---")
    raise ValueError("Test error from Node A")  # This should trigger the jump
    return {"messages": ["Message from Node A"]}

@error_handler()
async def node_b(state: GraphState):
    print("---Executing Node B---")
    return {"messages": ["Message from Node B"]}

async def exception_node(state: GraphState):
    print("---Executing Exception_node---")
    return {"messages": ["An error occurred"]}

# Build graph
workflow = StateGraph(GraphState)
workflow.add_node("A", node_a)
workflow.add_node("B", node_b)
workflow.add_node("exception_node", exception_node)

workflow.set_entry_point("A")
workflow.add_edge("A", "B")  # Explicit edge
workflow.add_edge("B", END)
workflow.add_edge("exception_node", END)

app = workflow.compile()

async def main():
    inputs = {"messages": []}
    print("\n---Running---")
    final_state = await app.ainvoke(inputs)
    print("---Final State---")
    print(final_state)

if __name__ == "__main__":
    asyncio.run(main())

Error Message and Stack Trace (if applicable)

Actual Output:
---Running---
Starting execution: node_a
---Executing Node A---
Error in node_a: Test error from Node A
Starting execution: node_b    # UNEXPECTED: Node B should not run
---Executing Node B---
Successfully completed: node_b
---Executing Exception_node---
---Final State---
{'messages': ['Message from Node B', 'An error occurred']}  # State merged from both paths



Expected Output:
---Running---
Starting execution: node_a
---Executing Node A---
Error in node_a: Test error from Node A
---Executing Exception_node---  # ONLY the exception node should run
---Final State---
{'messages': ['An error occurred']}

Description

I implemented a unified error-handling decorator for all nodes in a StateGraph. The decorator catches exceptions and attempts to redirect the flow to a dedicated exception_node by returning a Command(goto="exception_node").

However, when a node throws an exception, the graph does not interrupt its pre-defined execution flow set by add_edge. Instead, it seems to execute both paths: the error handler triggers, but then the graph also proceeds to the next node specified by the original edge. This results in the exception_node and the subsequent normal node both being executed, which is contrary to the expected behavior where the graph should immediately jump to the exception handler and stop the normal flow.

Expected Behavior:
When a node’s error-handling decorator catches an exception and returns a Command(goto="exception_node"), I expect the graph to interrupt the current explicit edge flow and immediately transition to the specified exception_node. The subsequent node (as defined by add_edge) should not execute.

Actual Behavior:
The graph appears to not respect the interrupt from the decorator’s Command when explicit edges are defined. Both the exception_node (from the goto command) and the next node in the original chain (from add_edge) are invoked, leading to merged and incorrect state updates.

finally, we solve this issue by all use command, but it should be a good method, we can't change all our project graph to not use addadge, we should have the uniform process enhance for the whole graph node

System Info

OS: Darwin
OS Version: Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:30 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T8132
Python Version: 3.12.11 (main, Dec 4 2025, 15:32:46) [Clang 17.0.0 (clang-1700.0.13.5)]

Originally created by @Juussticee on GitHub (Dec 10, 2025). Originally assigned to: @sydney-runkle on GitHub. ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/). - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python import functools import operator import asyncio from typing import TypedDict, List, Callable, Annotated from langgraph.errors import GraphInterrupt from langgraph.graph import StateGraph, END from langgraph.types import Command def error_handler(): def decorator(func: Callable): @functools.wraps(func) async def async_wrapper(*args, **kwargs): try: print(f"Starting execution: {func.__name__}") result = await func(*args, **kwargs) print(f"Successfully completed: {func.__name__}") return result except GraphInterrupt: raise except Exception as e: print(f"Error in {func.__name__}: {str(e)}") # Attempt to redirect to exception node return Command(goto="exception_node") return async_wrapper return decorator class GraphState(TypedDict): messages: Annotated[List[str], operator.add] @error_handler() async def node_a(state: GraphState): print("---Executing Node A---") raise ValueError("Test error from Node A") # This should trigger the jump return {"messages": ["Message from Node A"]} @error_handler() async def node_b(state: GraphState): print("---Executing Node B---") return {"messages": ["Message from Node B"]} async def exception_node(state: GraphState): print("---Executing Exception_node---") return {"messages": ["An error occurred"]} # Build graph workflow = StateGraph(GraphState) workflow.add_node("A", node_a) workflow.add_node("B", node_b) workflow.add_node("exception_node", exception_node) workflow.set_entry_point("A") workflow.add_edge("A", "B") # Explicit edge workflow.add_edge("B", END) workflow.add_edge("exception_node", END) app = workflow.compile() async def main(): inputs = {"messages": []} print("\n---Running---") final_state = await app.ainvoke(inputs) print("---Final State---") print(final_state) if __name__ == "__main__": asyncio.run(main()) ``` ### Error Message and Stack Trace (if applicable) ```shell Actual Output: ---Running--- Starting execution: node_a ---Executing Node A--- Error in node_a: Test error from Node A Starting execution: node_b # UNEXPECTED: Node B should not run ---Executing Node B--- Successfully completed: node_b ---Executing Exception_node--- ---Final State--- {'messages': ['Message from Node B', 'An error occurred']} # State merged from both paths Expected Output: ---Running--- Starting execution: node_a ---Executing Node A--- Error in node_a: Test error from Node A ---Executing Exception_node--- # ONLY the exception node should run ---Final State--- {'messages': ['An error occurred']} ``` ### Description I implemented a unified error-handling decorator for all nodes in a StateGraph. The decorator catches exceptions and attempts to redirect the flow to a dedicated exception_node by returning a Command(goto="exception_node"). However, when a node throws an exception, the graph does not interrupt its pre-defined execution flow set by add_edge. Instead, it seems to execute both paths: the error handler triggers, but then the graph also proceeds to the next node specified by the original edge. This results in the exception_node and the subsequent normal node both being executed, which is contrary to the expected behavior where the graph should immediately jump to the exception handler and stop the normal flow. Expected Behavior: When a node’s error-handling decorator catches an exception and returns a Command(goto="exception_node"), I expect the graph to interrupt the current explicit edge flow and immediately transition to the specified exception_node. The subsequent node (as defined by add_edge) should not execute. Actual Behavior: The graph appears to not respect the interrupt from the decorator’s Command when explicit edges are defined. Both the exception_node (from the goto command) and the next node in the original chain (from add_edge) are invoked, leading to merged and incorrect state updates. finally, we solve this issue by all use command, but it should be a good method, we can't change all our project graph to not use addadge, we should have the uniform process enhance for the whole graph node ### System Info OS: Darwin OS Version: Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:30 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T8132 Python Version: 3.12.11 (main, Dec 4 2025, 15:32:46) [Clang 17.0.0 (clang-1700.0.13.5)]
yindo added the bugpending labels 2026-02-20 17:43:01 -05:00
yindo closed this issue 2026-02-20 17:43:01 -05:00
Author
Owner

@sydney-runkle commented on GitHub (Dec 12, 2025):

Hiya, thanks for your question!

This is actually the expected behavior. Because node A (which is really wrapped in async_wrapper doesn't actually throw the exception, deterministic edges are followed.

Command results in additional paths being added, but doesn't prevent existing edges from being followed.

So the behavior you're seeing is expected. Perhaps you want a conditional edge to B that's dependent upon a successful A execution, as can be verified in state?

@sydney-runkle commented on GitHub (Dec 12, 2025): Hiya, thanks for your question! This is actually the expected behavior. Because node A (which is really wrapped in `async_wrapper` doesn't actually throw the exception, deterministic edges are followed. `Command` results in additional paths being added, but doesn't prevent existing edges from being followed. So the behavior you're seeing is expected. Perhaps you want a conditional edge to `B` that's dependent upon a successful `A` execution, as can be verified in state?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#1088