Skip to content

error_handler runs but the original exception is still raised (parallel nodes, custom/messages streaming, subgraphs=True) #8608

Description

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 LangGraph rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (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.

Related Issues / PRs

No response

Reproduction Steps / Example Code (Python)

import asyncio

from typing_extensions import TypedDict

from langgraph.errors import NodeError
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command

handler_calls = 0


class State(TypedDict, total=False):
    x: str
    y: str


def boom(state: State) -> State:
    raise RuntimeError("test")


def sibling(state: State) -> State:
    return {"y": "sibling"}


def handler(state: State, error: NodeError) -> Command:
    global handler_calls
    handler_calls += 1
    return Command(update={"x": "handled"}, goto=END)


def build(parallel: bool):
    """`parallel=True` puts a second node in the same superstep as the failing one."""
    builder = (
        StateGraph(State)
        .add_node("boom", boom, error_handler=handler)
        .add_edge(START, "boom")
    )
    if parallel:
        builder = builder.add_node("sibling", sibling).add_edge(START, "sibling")
    return builder.compile()


async def collect(graph, **kwargs):
    return [event async for event in graph.astream({}, **kwargs)]


CASES = [
    ("solo",     "invoke",                   lambda g: g.invoke({})),
    ("solo",     "stream values",            lambda g: list(g.stream({}, stream_mode="values"))),
    ("solo",     "stream updates",           lambda g: list(g.stream({}, stream_mode="updates"))),
    ("solo",     "stream custom",            lambda g: list(g.stream({}, stream_mode="custom"))),
    ("solo",     "stream messages",          lambda g: list(g.stream({}, stream_mode="messages"))),
    ("solo",     "stream values, subgraphs", lambda g: list(g.stream({}, stream_mode="values", subgraphs=True))),
    ("solo",     "ainvoke",                  lambda g: asyncio.run(g.ainvoke({}))),
    ("solo",     "astream custom",           lambda g: asyncio.run(collect(g, stream_mode="custom"))),
    ("parallel", "invoke",                   lambda g: g.invoke({})),
    ("parallel", "stream values",            lambda g: list(g.stream({}, stream_mode="values"))),
    ("parallel", "ainvoke",                  lambda g: asyncio.run(g.ainvoke({}))),
]

for graph_kind, how, run in CASES:
    handler_calls = 0
    try:
        outcome = f"ok      {run(build(graph_kind == 'parallel'))}"
    except Exception as exc:
        outcome = f"RAISED  {exc!r}"
    print(f"{graph_kind:9} {how:26} handler ran {handler_calls}x  {outcome}")

Error Message and Stack Trace (if applicable)

From the `parallel` / `invoke` case, with the `try`/`except` taken out so the traceback shows:

Traceback (most recent call last):
  File ".../langgraph/libs/langgraph/test.py", line 64, in <module>
    outcome = f"ok      {run(build(graph_kind == 'parallel'))}"
                         ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../langgraph/libs/langgraph/test.py", line 56, in <lambda>
    ("parallel", "invoke",                   lambda g: g.invoke({})),
                                                       ~~~~~~~~^^^^
  File ".../langgraph/libs/langgraph/langgraph/pregel/main.py", line 3913, in invoke
    for chunk in self.stream(
                 ~~~~~~~~~~~^
        input,
        ^^^^^^
    ...<11 lines>...
        **kwargs,
        ^^^^^^^^^
    ):
    ^
  File "...langgraph/libs/langgraph/langgraph/pregel/main.py", line 2899, in stream
    with SyncPregelLoop(
         ~~~~~~~~~~~~~~^
        input,
        ^^^^^^
    ...<18 lines>...
        has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ) as loop:
    ^
  File "...langgraph/libs/langgraph/langgraph/pregel/_loop.py", line 1719, in __exit__
    return self.stack.__exit__(exc_type, exc_value, traceback)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/contextlib.py", line 619, in __exit__
    raise exc
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/contextlib.py", line 604, in __exit__
    if cb(*exc_details):
       ~~^^^^^^^^^^^^^^
  File ".../langgraph/libs/langgraph/langgraph/pregel/_executor.py", line 117, in __exit__
    task.result()
    ~~~~~~~~~~~^^
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/_base.py", line 443, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/_base.py", line 395, in __get_result
    raise self._exception
  File ".../langgraph/libs/langgraph/langgraph/pregel/_executor.py", line 80, in done
    task.result()
    ~~~~~~~~~~~^^
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/_base.py", line 443, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/_base.py", line 395, in __get_result
    raise self._exception
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/thread.py", line 86, in run
    result = ctx.run(self.task)
  File ".../cpython-3.14.4-macos-aarch64-none/lib/python3.14/concurrent/futures/thread.py", line 73, in run
    return fn(*args, **kwargs)
  File ".../langgraph/libs/langgraph/langgraph/pregel/_retry.py", line 617, in run_with_retry
    return task.proc.invoke(task.input, config)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File ".../langgraph/libs/langgraph/langgraph/_internal/_runnable.py", line 707, in invoke
    input = context.run(step.invoke, input, config, **kwargs)
  File ".../langgraph/libs/langgraph/langgraph/_internal/_runnable.py", line 447, in invoke
    ret = self.func(*args, **kwargs)
  File ".../langgraph/libs/langgraph/test.py", line 18, in boom
    raise RuntimeError("test")
RuntimeError: test
During task with name 'boom' and id '55a0601e-c332-8b49-8d5f-a53824502795'

Description

  • I'm using add_node(..., error_handler=...) to catch a node exception, apply the handler's Command update, and let the graph finish cleanly.
  • I expect that to work regardless of what else is running or how I'm streaming.
  • Instead the original exception still comes out of the graph in most configurations. It only stays caught for a lone failing node run via invoke/ainvoke or stream_mode="values"/"updates". Put a second node in the same superstep and even plain invoke raises; keep the node on its own but stream with stream_mode="custom", "messages", or subgraphs=True and it raises too.

Output of the script above:

solo      invoke                     handler ran 1x  ok      {'x': 'handled'}
solo      stream values              handler ran 1x  ok      [{'x': 'handled'}]
solo      stream updates             handler ran 1x  ok      [{'__error_handler__boom': {'x': 'handled'}}]
solo      stream custom              handler ran 1x  RAISED  RuntimeError('test')
solo      stream messages            handler ran 1x  RAISED  RuntimeError('test')
solo      stream values, subgraphs   handler ran 1x  RAISED  RuntimeError('test')
solo      ainvoke                    handler ran 1x  ok      {'x': 'handled'}
solo      astream custom             handler ran 1x  RAISED  RuntimeError('test')
parallel  invoke                     handler ran 1x  RAISED  RuntimeError('test')
parallel  stream values              handler ran 1x  RAISED  RuntimeError('test')
parallel  ainvoke                    handler ran 1x  RAISED  RuntimeError('test')

The handler runs exactly once in every case, including the failing ones, but the exception is just re-raised anyway afterwards. The ok rows are there as contrast; those are the configurations where it behaves as documented.

Reproduced on a clean checkout of main at 644815f (tag 1.2.11, no local modifications).

If you agree this is a bug, I'm happy to fix it and open a PR.

System Info

System Information

OS: Darwin
OS Version: Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:43 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T8132
Python Version: 3.14.4 (main, Apr 14 2026, 14:46:33) [Clang 22.1.3 ]

Package Information

langchain_core: 1.5.3
langsmith: 0.8.18
langchain_protocol: 0.0.18
langgraph_sdk: 0.4.2

Optional packages not installed

deepagents
deepagents-cli

Other Dependencies

httpx: 0.28.1
jsonpatch: 1.33
orjson: 3.11.9
packaging: 25.0
pydantic: 2.13.4
pytest: 9.1.1
pyyaml: 6.0.3
requests: 2.33.1
requests-toolbelt: 1.0.0
tenacity: 9.1.2
typing-extensions: 4.15.0
uuid-utils: 0.13.0
websockets: 16.0
xxhash: 3.8.1
zstandard: 0.25.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions