Claude API Tool Use Error Handling: Examples & Patterns
When Claude calls a tool in your application, there are several points where things can go wrong: Claude might generate malformed input, your tool execution might fail, the tool might time out, or Claude might misuse a tool it has access to. 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) feature, covering the most common failure modes and how to recover from each one gracefully.
The Tool Use Error Categories
Before writing error handling code, it helps to classify what can actually fail:
- Input validation errors — Claude generates a
tool_useblock with arguments that don't match your schema (wrong type, missing required field, invalid enum value). - Execution errors — your tool's code runs but throws (API down, database timeout, permission denied).
- Logical errors — the tool runs fine but returns a result Claude can't use productively (empty search results, a 404 from a downstream API).
- Protocol errors — your code fails to send back a properly formatted
tool_resultblock, breaking the conversation.
Each category needs a different handling strategy, and mixing them up is the most common bug in tool-use implementations.
Pattern 1: Validate Before You Execute
Never pass Claude's tool input directly into your business logic. Validate it first, and if validation fails, return the error as a tool result, not as an exception that crashes your handler.
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,
content: "Error: 'location' must be a non-empty string.",
is_error: true
};
}
// proceed with valid input
}
}
Setting is_error: true is important: it tells Claude the call failed so it can decide whether to retry with corrected input, ask the user for clarification, or try a different approach. Silently returning an empty string instead looks like a successful-but-useless result, and Claude will often hallucinate around it.
Pattern 2: Catch Execution Failures and Surface Them as Tool Results
Wrap every tool execution in a try/catch and convert exceptions into structured tool_result content rather than letting them propagate and kill the whole request.
async function runTool(toolUse) {
try {
const result = await executeTool(toolUse.name, 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,
content: `Error executing ${toolUse.name}: ${err.message}`,
is_error: true
};
}
}
This keeps the conversation loop alive: Claude sees the error, and in most cases will either retry, adjust its approach, or explain the failure to the end user instead of your app returning a raw 500.
Pattern 3: Timeouts Need Explicit Handling
Tools that call external APIs or databases can hang. Always wrap execution in a timeout and return a clear, actionable error message — vague errors lead Claude to retry the same failing call in a loop.
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Tool call timed out")), ms)
)
]);
}
try {
const result = await withTimeout(callExternalApi(input), 8000);
} catch (err) {
return {
type: "tool_result",
tool_use_id: toolUse.id,
content: "Error: the external service did not respond in time. Do not retry immediately.",
is_error: true
};
}
Including a hint like "do not retry immediately" in the error content helps steer Claude away from repeated identical calls when the underlying issue is a down dependency, not a bad input.
Pattern 4: Guard Against Infinite Tool-Call Loops
If a tool keeps failing, Claude may keep calling it with slightly different arguments. Track call counts per tool within a single conversation turn and cut it off:
const MAX_CALLS_PER_TOOL = 3;
const callCounts = {};
function checkCallLimit(toolName) {
callCounts[toolName] = (callCounts[toolName] || 0) + 1;
if (callCounts[toolName] > MAX_CALLS_PER_TOOL) {
return {
is_error: true,
content: `Tool '${toolName}' has failed repeatedly. Stop retrying and inform the user.`
};
}
return null;
}
This protects both your API costs and your downstream services from runaway retry behavior.
Pattern 5: Partial Failures in Multi-Tool Turns
When Claude requests multiple tool calls in one turn, one may succeed and another may fail. Send back all tool_result blocks in a single user message — don't drop the failed one or send it separately.
const toolResults = await Promise.all(
toolUseBlocks.map(block => runTool(block))
);
messages.push({
role: "user",
content: toolResults
});
Missing a tool_result for any tool_use_id Claude sent will break the conversation state and usually produces a 400-level error on the next request.
Where SubToAPI Fits
If you're exposing Claude's tool use capability through your own API endpoints, SubToAPI gives you application-level sub_live_... keys, request logging, and usage metadata on top of standard Claude messages and tool calls — useful when you need to debug which tool call failed for which customer without building that tracking yourself. Tool use requests work the same way as direct Claude API calls; see the tool use docs and messages reference for the exact request shape. You can test these patterns with a free trial at signup.
Questions
What does is_error: true actually do in a tool result? It flags the content as a failure so Claude treats it as an error signal rather than a normal result. Claude uses this to decide whether to retry, adjust input, try a different tool, or report the failure to the user instead of continuing as if the call succeeded.
Should I retry failed tool calls automatically on my side? Only for transient infrastructure errors (timeouts, 503s) with a short backoff and a retry cap. For validation or logic errors, let the error flow back to Claude as a tool_result so it can self-correct — automatically retrying bad input just repeats the same failure.
What happens if I forget to send a tool_result for a tool call Claude made? The next request will typically fail with a 400-level error because the conversation has an unresolved tool_use_id. Every tool_use block Claude sends must get a matching tool_result, even if that result is just an error message.