Skip to content

Unexpected Execution Flow When Unified Error-Handling Decorator Conflicts with Explicitly Defined Edges in StateGraph #34278

@chenxiaofeigit

Description

@chenxiaofeigit

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-cli
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • langchain-chroma
  • langchain-deepseek
  • langchain-exa
  • langchain-fireworks
  • langchain-groq
  • langchain-huggingface
  • langchain-mistralai
  • langchain-nomic
  • langchain-ollama
  • langchain-perplexity
  • langchain-prompty
  • langchain-qdrant
  • langchain-xai
  • Other / not sure / general

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)

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

System Information

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)]

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugRelated to a bug, vulnerability, unexpected error with an existing featurecore`langchain-core` package issues & PRslangchain`langchain` package issues & PRs

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions