← Blog

Claude Tool Use Error Handling Examples

2026-09-27 · 5 min read · SubToAPI Team

When Claude calls a tool, plenty of things can go wrong before you ever see a final answer: the model might send malformed arguments, your function might throw, an external API might time out, or Claude might call a tool that doesn't exist. Handling these cases correctly is what separates a demo from a production integration. This article walks through concrete error handling examples for Claude's tool use (function calling), including how to structure tool_result blocks so Claude can recover gracefully instead of looping or hallucinating.

The short answer: you must always return a tool_result for every tool_use block, even when the tool call failed, and you should set is_error: true so Claude knows to adjust its next move rather than treat the failed output as valid data. Below are the patterns that actually work.

The basic tool_result error contract

Every tool call Claude makes produces a tool_use content block with an id. Your job is to execute it and send back a tool_result block referencing that same id. If something goes wrong, the shape is the same — you just flip is_error and put a useful message in content.

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A9x...",
      "is_error": true,
      "content": "Error: division by zero in calculate_discount"
    }
  ]
}

Claude reads this like a normal tool output but treats the is_error flag as a signal to reconsider its approach — retry with different arguments, ask the user for clarification, or fall back to a different tool.

Example 1: Malformed or missing arguments

Sometimes Claude sends arguments that don't match your schema — a string where you expected a number, a missing required field, or an enum value that doesn't exist. Validate before executing, and return the validation error as the tool result instead of throwing an unhandled exception.

function handleToolCall(toolUse) {
  const { name, input, id } = toolUse;

  if (name === "get_weather") {
    if (typeof input.location !== "string" || !input.location.trim()) {
      return {
        type: "tool_result",
        tool_use_id: id,
        is_error: true,
        content: "Error: 'location' must be a non-empty string"
      };
    }
    // proceed with valid input
  }
}

This gives Claude enough information to correct itself on the next turn — usually it will re-call the tool with a fixed location value rather than giving up.

Example 2: Tool execution failures (network, database, timeout)

External calls fail. Wrap the actual execution in a try/catch and return a descriptive but non-sensitive error message — don't leak stack traces or internal system details into the conversation.

async function executeTool(toolUse) {
  try {
    const result = await callInternalAPI(toolUse.input);
    return {
      type: "tool_result",
      tool_use_id: toolUse.id,
      content: JSON.stringify(result)
    };
  } catch (err) {
    return {
      type: "tool_result",
      tool_use_id: toolUse.id,
      is_error: true,
      content: `Error: ${err.name === "TimeoutError" ? "request timed out, try again" : "service unavailable"}`
    };
  }
}

For timeouts specifically, it helps to tell Claude explicitly that a retry is reasonable — otherwise it may assume the operation is permanently unavailable and stop trying.

Example 3: Unknown tool name

If Claude hallucinates a tool name that isn't in your tools array (rare, but possible with a long tool list or a poorly worded system prompt), don't crash your handler — return an error result naming the valid tools.

const knownTools = ["get_weather", "search_orders", "create_ticket"];

if (!knownTools.includes(toolUse.name)) {
  return {
    type: "tool_result",
    tool_use_id: toolUse.id,
    is_error: true,
    content: `Error: unknown tool "${toolUse.name}". Available tools: ${knownTools.join(", ")}`
  };
}

Example 4: Partial failure in parallel tool calls

Claude can request multiple tools in a single turn. If one succeeds and another fails, send back both results in the same user message, each with its own tool_use_id. Don't drop the failed one or send it as a separate turn — that breaks the conversation's tool-call/result pairing.

{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_01", "content": "72°F, sunny" },
    { "type": "tool_result", "tool_use_id": "toolu_02", "is_error": true, "content": "Error: order ID not found" }
  ]
}

Example 5: Retry logic with a cap

A common failure mode is Claude retrying a failing tool indefinitely because the error message is too vague ("Error: failed"). Be specific about why it failed and, after a couple of attempts, tell Claude to stop retrying and ask the user instead.

if (retryCount >= 2) {
  return {
    type: "tool_result",
    tool_use_id: toolUse.id,
    is_error: true,
    content: "Error: tool has failed twice with these inputs. Do not retry — ask the user for different input or explain the limitation."
  };
}

This kind of explicit instruction inside the error content is more reliable than hoping the model infers it.

If you're proxying Claude through an API layer

If you're exposing Claude's tool use through your own backend or a third-party layer like SubToAPI, the same rules apply — the error handling happens at the application layer, not inside the API itself. SubToAPI passes through Claude's native tool_use and tool_result format unchanged, so any error-handling code you write against the standard Messages API works without modification. If you're evaluating providers, the quickstart and messages docs show the exact request/response shapes, including streaming tool calls, which is covered separately in the streaming docs.

Checklist for robust tool error handling

Questions

Do I need to set is_error or is a descriptive message enough? Set is_error: true explicitly. Claude uses it as a signal distinct from the text content, and it affects how the model weighs the result in its next reasoning step.

What happens if I don't return a tool_result at all? The conversation becomes invalid — Claude's API expects a tool_result for every pending tool_use_id before it will generate further text, so omitting one typically causes a request error on the next turn.

Can Claude retry a failed tool call automatically? Yes, if your error message makes clear that a retry with different input is appropriate. Without that context, Claude may give up, ask the user, or repeat the same failing call — so be explicit in the error content.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →