Skip to content

Tool node error handling disabled by default after 1.0.1 #6486

Description

@akiliscodes

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

from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langchain_core.messages import HumanMessage, ToolMessage, AIMessage
from langchain_core.tools import tool
from typing import TypedDict, List


class State(TypedDict):
    messages: List


@tool
def exploding_tool():
    """A tool that always raises an exception."""
    raise ValueError("boom")


def agent_node(state: State):
    # return an AIMessage containing a tool call
    return {
        "messages": [
            AIMessage(
                content="calling tool",
                tool_calls=[{
                    "name": "exploding_tool",
                    "args": {},
                    "id": "test-call"
                }]
            )
        ]
    }


graph = StateGraph(State)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode([exploding_tool]))

graph.add_edge(START, "agent")
graph.add_edge("agent", "tools")
graph.add_edge("tools", END)

app = graph.compile()

result = app.invoke({
    "messages": [
        HumanMessage(content="hello")
    ]
})

print("\n=== RESULT ===")
print(result)

Error Message and Stack Trace (if applicable)

ValueError                                Traceback (most recent call last)
Cell In[3], line 44
     40 graph.add_edge("tools", END)
     42 app = graph.compile()
---> 44 result = app.invoke({
     45     "messages": [
     46         HumanMessage(content="hello")
     47     ]
     48 })
     50 print("\n=== RESULT ===")
     51 print(result)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/pregel/main.py:3050, in Pregel.invoke(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, **kwargs)
   3047 chunks: list[dict[str, Any] | Any] = []
   3048 interrupts: list[Interrupt] = []
-> 3050 for chunk in self.stream(
   3051     input,
   3052     config,
   3053     context=context,
   3054     stream_mode=["updates", "values"]
   3055     if stream_mode == "values"
   3056     else stream_mode,
   3057     print_mode=print_mode,
   3058     output_keys=output_keys,
   3059     interrupt_before=interrupt_before,
   3060     interrupt_after=interrupt_after,
   3061     durability=durability,
   3062     **kwargs,
   3063 ):
   3064     if stream_mode == "values":
   3065         if len(chunk) == 2:

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/pregel/main.py:2633, in Pregel.stream(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)
   2631 for task in loop.match_cached_writes():
   2632     loop.output_writes(task.id, task.writes, cached=True)
-> 2633 for _ in runner.tick(
   2634     [t for t in loop.tasks.values() if not t.writes],
   2635     timeout=self.step_timeout,
   2636     get_waiter=get_waiter,
   2637     schedule_task=loop.accept_push,
   2638 ):
   2639     # emit output
   2640     yield from _output(
   2641         stream_mode, print_mode, subgraphs, stream.get, queue.Empty
   2642     )
   2643 loop.after_tick()

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/pregel/_runner.py:167, in PregelRunner.tick(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)
    165 t = tasks[0]
    166 try:
--> 167     run_with_retry(
    168         t,
    169         retry_policy,
    170         configurable={
    171             CONFIG_KEY_CALL: partial(
    172                 _call,
    173                 weakref.ref(t),
    174                 retry_policy=retry_policy,
    175                 futures=weakref.ref(futures),
    176                 schedule_task=schedule_task,
    177                 submit=self.submit,
    178             ),
    179         },
    180     )
    181     self.commit(t, None)
    182 except Exception as exc:

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/pregel/_retry.py:42, in run_with_retry(task, retry_policy, configurable)
     40     task.writes.clear()
     41     # run the task
---> 42     return task.proc.invoke(task.input, config)
     43 except ParentCommand as exc:
     44     ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:656, in RunnableSeq.invoke(self, input, config, **kwargs)
    654     # run in context
    655     with set_config_context(config, run) as context:
--> 656         input = context.run(step.invoke, input, config, **kwargs)
    657 else:
    658     input = step.invoke(input, config)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:400, in RunnableCallable.invoke(self, input, config, **kwargs)
    398         run_manager.on_chain_end(ret)
    399 else:
--> 400     ret = self.func(*args, **kwargs)
    401 if self.recurse and isinstance(ret, Runnable):
    402     return ret.invoke(input, config)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:799, in ToolNode._func(self, input, config, runtime)
    797 input_types = [input_type] * len(tool_calls)
    798 with get_executor_for_config(config) as executor:
--> 799     outputs = list(
    800         executor.map(self._run_one, tool_calls, input_types, tool_runtimes)
    801     )
    803 return self._combine_tool_outputs(outputs, input_type)

File ~/miniconda/envs/my_env/lib/python3.13/concurrent/futures/_base.py:619, in Executor.map.<locals>.result_iterator()
    616 while fs:
    617     # Careful not to keep a reference to the popped future
    618     if timeout is None:
--> 619         yield _result_or_cancel(fs.pop())
    620     else:
    621         yield _result_or_cancel(fs.pop(), end_time - time.monotonic())

File ~/miniconda/envs/my_env/lib/python3.13/concurrent/futures/_base.py:317, in _result_or_cancel(***failed resolving arguments***)
    315 try:
    316     try:
--> 317         return fut.result(timeout)
    318     finally:
    319         fut.cancel()

File ~/miniconda/envs/my_env/lib/python3.13/concurrent/futures/_base.py:449, in Future.result(self, timeout)
    447     raise CancelledError()
    448 elif self._state == FINISHED:
--> 449     return self.__get_result()
    451 self._condition.wait(timeout)
    453 if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:

File ~/miniconda/envs/my_env/lib/python3.13/concurrent/futures/_base.py:401, in Future.__get_result(self)
    399 if self._exception:
    400     try:
--> 401         raise self._exception
    402     finally:
    403         # Break a reference cycle with the exception in self._exception
    404         self = None

File ~/miniconda/envs/my_env/lib/python3.13/concurrent/futures/thread.py:59, in _WorkItem.run(self)
     56     return
     58 try:
---> 59     result = self.fn(*self.args, **self.kwargs)
     60 except BaseException as exc:
     61     self.future.set_exception(exc)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langchain_core/runnables/config.py:546, in ContextThreadPoolExecutor.map.<locals>._wrapped_fn(*args)
    545 def _wrapped_fn(*args: Any) -> T:
--> 546     return contexts.pop().run(fn, *args)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:1010, in ToolNode._run_one(self, call, input_type, tool_runtime)
   1006 config = tool_runtime.config
   1008 if self._wrap_tool_call is None:
   1009     # No wrapper - execute directly
-> 1010     return self._execute_tool_sync(tool_request, input_type, config)
   1012 # Define execute callable that can be called multiple times
   1013 def execute(req: ToolCallRequest) -> ToolMessage | Command:

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:916, in ToolNode._execute_tool_sync(self, request, input_type, config)
    914 try:
    915     try:
--> 916         response = tool.invoke(call_args, config)
    917     except ValidationError as exc:
    918         # Filter out errors for injected arguments
    919         injected = self._injected_args.get(call["name"])

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langchain_core/tools/base.py:605, in BaseTool.invoke(self, input, config, **kwargs)
    597 @override
    598 def invoke(
    599     self,
   (...)
    602     **kwargs: Any,
    603 ) -> Any:
    604     tool_input, kwargs = _prep_run_args(input, config, **kwargs)
--> 605     return self.run(tool_input, **kwargs)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langchain_core/tools/base.py:932, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs)
    930 if error_to_raise:
    931     run_manager.on_tool_error(error_to_raise)
--> 932     raise error_to_raise
    933 output = _format_output(content, artifact, tool_call_id, self.name, status)
    934 run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langchain_core/tools/base.py:898, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs)
    896     if config_param := _get_runnable_config_param(self._run):
    897         tool_kwargs |= {config_param: config}
--> 898     response = context.run(self._run, *tool_args, **tool_kwargs)
    899 if self.response_format == "content_and_artifact":
    900     msg = (
    901         "Since response_format='content_and_artifact' "
    902         "a two-tuple of the message content and raw tool output is "
    903         f"expected. Instead, generated response is of type: "
    904         f"{type(response)}."
    905     )

File ~/miniconda/envs/my_env/lib/python3.13/site-packages/langchain_core/tools/structured.py:93, in StructuredTool._run(self, config, run_manager, *args, **kwargs)
     91     if config_param := _get_runnable_config_param(self.func):
     92         kwargs[config_param] = config
---> 93     return self.func(*args, **kwargs)
     94 msg = "StructuredTool does not support sync invocation."
     95 raise NotImplementedError(msg)

Cell In[3], line 15
     12 @tool
     13 def exploding_tool():
     14     """A tool that always raises an exception."""
---> 15     raise ValueError("boom")

ValueError: boom
During task with name 'tools' and id 'f1c7f354-d5cf-e2d9-fe91-2088176594ed'

Description

Hello,

After updating to langgraph-prebuilt 1.0.1 (via langgraph 1.0.2), I noticed a change in the default behavior of tool error handling.

Previously, tool nodes would automatically handle errors.
After this commit:

4ac1c62#diff-34ed372d5e45913593e00f5704aa61f05ebc1d0aafbfd032f12b29c0db448cbeL126

…the default changed, and tool error handling is now off unless explicitly enabled.
This modifies the behavior of existing graphs that relied on the old default.

Expected

Tool node error handling enabled by default, matching earlier versions.

Actual

Tool errors now propagate unless manually enabling tool_node_handling like below

graph.add_node("tools", ToolNode([exploding_tool], handle_tool_errors=True))

Thanks for the work on the project!
Just reporting this in case the behavior change was unintentional.

System Info

System Information

OS: Darwin
OS Version: Darwin Kernel Version 24.5.0: Tue Apr 22 19:48:46 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T8103
Python Version: 3.13.1 | packaged by conda-forge | (main, Jan 8 2025, 09:15:43) [Clang 18.1.8 ]

Package Information

langchain_core: 1.1.0
langsmith: 0.4.46
langgraph_sdk: 0.2.9

Optional packages not installed

langserve

Other Dependencies

httpx: 0.28.1
jsonpatch: 1.33
orjson: 3.11.4
packaging: 25.0
pydantic: 2.12.4
pyyaml: 6.0.3
requests: 2.32.5
requests-toolbelt: 1.0.0
tenacity: 9.1.2
typing-extensions: 4.15.0
zstandard: 0.25.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternalpendingawaiting review/confirmation by maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions