Skip to content

ToolNode throws unhandled error for invalid tool names before error handling can intercept #9843

@john-rtr

Description

@john-rtr

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 very descriptive title to this issue.
  • I searched the LangChain.js documentation with the integrated search.
  • 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.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

Example Code

import { createAgent, FakeToolCallingModel } from 'langchain'
import { z } from 'zod'
import { tool } from '@langchain/core/tools'

async function main() {
  // Define a simple tool
  const getWeather = tool(({ location }) => `Weather in ${location}: sunny`, {
    name: 'get_weather',
    description: 'Get weather for a location',
    schema: z.object({
      location: z.string()
    })
  })

  // Create a fake model that returns an invalid tool call
  const fakeModel = new FakeToolCallingModel({
    toolCalls: [
      // First call: returns invalid tool name
      [
        {
          name: 'nonexistent_tool', // This tool doesn't exist!
          args: { foo: 'bar' },
          id: 'call_123'
        }
      ],
      // Second call: empty (final response)
      []
    ]
  })

  const agent = createAgent({
    model: fakeModel,
    tools: [getWeather]
  })

  console.log('Testing createAgent with invalid tool call...')
  console.log('Expected: Agent should handle error gracefully')
  console.log('Actual:\n')

  try {
    const result = await agent.invoke({
      messages: [{ role: 'user', content: "What's the weather?" }]
    })
    console.log('✅ Agent completed')
    console.log('Last message:', result.messages[result.messages.length - 1].content)
  } catch (error: any) {
    console.error('❌ UNHANDLED ERROR:', error.message)
    console.error('\nStack:', error.stack)
  }
}

main()

Error Message and Stack Trace (if applicable)

Testing createAgent with invalid tool call...
Expected: Agent should handle error gracefully
Actual:

❌ UNHANDLED ERROR: Tool "nonexistent_tool" not found.

Stack: Error: Tool "nonexistent_tool" not found.
    at ToolNode.runTool (/.pnpm/langchain@1.2.7_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter_e96464e95e75121a98af3e7edea629c2/node_modules/langchain/src/agents/nodes/ToolNode.ts:350:13)
    at ToolNode.run (/.pnpm/langchain@1.2.7_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter_e96464e95e75121a98af3e7edea629c2/node_modules/langchain/src/agents/nodes/ToolNode.ts:395:29)
    at ToolNode.func (/.pnpm/langchain@1.2.7_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter_e96464e95e75121a98af3e7edea629c2/node_modules/langchain/src/agents/nodes/ToolNode.ts:194:14)
    at <anonymous> (/.pnpm/langchain@1.2.7_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter_e96464e95e75121a98af3e7edea629c2/node_modules/langchain/src/agents/RunnableCallable.ts:73:24)
    at AsyncLocalStorage.run (node:internal/async_local_storage/async_context_frame:63:14)
    at AsyncLocalStorageProvider.runWithConfig (/.pnpm/@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter-trace-otlp-prot_9973472e3d75ca47e707f3f4889b068e/node_modules/@langchain/core/src/singletons/async_local_storage/index.ts:88:20)
    at ToolNode.invoke (/.pnpm/langchain@1.2.7_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter_e96464e95e75121a98af3e7edea629c2/node_modules/langchain/src/agents/RunnableCallable.ts:71:66)
    at RunnableSequence.invoke (/.pnpm/@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@opentelemetry+exporter-trace-otlp-prot_9973472e3d75ca47e707f3f4889b068e/node_modules/@langchain/core/src/runnables/base.ts:1899:30)
    at async _runWithRetry (/.pnpm/@langchain+langgraph@1.0.15_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@openteleme_874695521d2034e85775d62b8ebe59d3/node_modules/@langchain/langgraph/src/pregel/retry.ts:103:16)
    at async PregelRunner._executeTasksWithRetry (/.pnpm/@langchain+langgraph@1.0.15_@langchain+core@1.1.12_@opentelemetry+api@1.9.0_@openteleme_874695521d2034e85775d62b8ebe59d3/node_modules/@langchain/langgraph/src/pregel/runner.ts:330:27)

Description

When ToolNode receives a tool call with a name that doesn't match any registered tool (e.g., when an LLM hallucinates a tool name) it throws an unhandled error instead of returning a ToolMessage with status: "error".

This happens because there are two "tool not found" checks in runTool():

  1. Inside baseHandler (line ~290-292) - this IS wrapped by try-catch
  2. Outside any try-catch (line ~348-351) - this throws BEFORE error handling
// This check runs BEFORE the try-catch blocks
const tool = this.tools.find((t) => t.name === call.name);
if (!tool) {
  throw new Error(`Tool "${call.name}" not found.`);  // Unhandled!
}

The error at line ~350 is thrown before reaching either:

  • The wrapToolCall middleware try-catch (line ~361-371)
  • The direct baseHandler try-catch (line ~377-385)

Expected behavior
When handleToolErrors is true (default), invalid tool names should be caught and returned as a ToolMessage with the error, allowing the LLM to see its mistake and retry. This is how it works in @langchain/langgraph's ToolNode.

Current behavior
The error propagates up and crashes the agent.

System Info

langchain@1.2.11
MacOS
Node 24.12.0
pnpm 10.25

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions