Claude API Function Calling Error Recovery Guide
When Claude calls a tool with malformed arguments, when your tool execution throws, or when Claude gets stuck in a loop retrying a broken function call, you need a recovery strategy — not just a try/catch that crashes the request. This article covers the concrete failure modes in Claude API function calling (tool use) and how to recover from each one without losing the conversation state or confusing the model further.
The short answer: treat tool errors as data to feed back to Claude, not exceptions to swallow silently. Claude's tool use protocol is designed around a feedback loop — you send a tool_result block back with the error content, and Claude adjusts its next call. Recovery almost always means catching the error, formatting it as a tool result, and letting the model retry with that new information, rather than retrying blindly on your side.
Why function calling errors happen
Tool use errors in the Claude API generally fall into four categories:
- Malformed or incomplete arguments — Claude generates a tool call with a missing required field, wrong type, or invalid enum value.
- Tool execution failures — your actual function throws (network timeout, database error, invalid ID, permission denied).
- Schema mismatches — your tool definition changed but the model's context still reflects an older version, or the JSON schema is ambiguous enough that Claude guesses wrong.
- Infinite or repeated retry loops — Claude keeps calling the same broken tool with slightly varied arguments because the error message you returned wasn't specific enough.
Each of these needs a different recovery pattern. None of them should be handled by just returning a generic 500 to your end user.
Pattern 1: Return structured errors as tool_result content
Claude's tool use flow expects a tool_result content block matching the tool_use_id. If your function fails, don't omit the result — always return one, with is_error: true and a clear, actionable message.
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"is_error": true,
"content": "Error: user_id 'u_8842' not found. Valid user_ids must be UUIDs starting with 'usr_'."
}
The key detail: be specific about what was wrong and what a valid value looks like. Claude uses the error text to reformulate its next call. Vague messages like "failed" or "error 500" lead to repeated failed attempts because the model has nothing to correct against.
Pattern 2: Validate before execution, not after
Most recoverable errors are argument errors, and you can catch them before they ever touch your backend by validating against your tool's JSON schema first.
function validateToolInput(toolName, input, schema) {
const errors = [];
for (const [key, rule] of Object.entries(schema.required || {})) {
if (!(key in input)) errors.push(`Missing required field: ${key}`);
}
return errors;
}
const errors = validateToolInput("get_order", toolCall.input, orderSchema);
if (errors.length) {
return {
type: "tool_result",
tool_use_id: toolCall.id,
is_error: true,
content: `Validation failed: ${errors.join("; ")}`
};
}
This cuts your execution-layer error rate significantly because you're rejecting bad calls before they hit a database or third-party API, and the error message you control is always more useful than whatever exception your downstream system throws.
Pattern 3: Cap retries and escalate
Claude will retry a tool call if you tell it something went wrong, but without a limit it can loop several times on the same bad input, especially if the underlying issue isn't something it can fix (a permissions error, a down dependency). Track retry counts per tool call in your conversation loop and break out after a threshold:
let toolRetries = {};
function handleToolError(toolUseId, toolName, errorMsg) {
toolRetries[toolName] = (toolRetries[toolName] || 0) + 1;
if (toolRetries[toolName] >= 3) {
return {
type: "tool_result",
tool_use_id: toolUseId,
is_error: true,
content: "This action is unavailable right now. Please inform the user and stop retrying this tool."
};
}
return {
type: "tool_result",
tool_use_id: toolUseId,
is_error: true,
content: errorMsg
};
}
The explicit instruction in the final error message ("stop retrying this tool") matters — it's a direct signal in-band that changes Claude's next action instead of relying on it to infer the loop is pointless.
Pattern 4: Handle partial failures in parallel tool calls
When Claude issues multiple tool calls in one turn, one can fail while others succeed. Don't drop the whole batch on one failure — return a tool_result for every tool_use block, successes and failures alike, in the same user turn. Claude needs a result for each call ID or the next request will be malformed.
Pattern 5: Distinguish transient from permanent errors
Network timeouts and rate limits are transient — worth a silent retry on your side before ever telling Claude. Schema violations and "not found" errors are permanent from the model's perspective within that turn — those should go straight back as tool results. Mixing these up either wastes model calls retrying things only your backend can fix, or hides retryable failures behind a model-level error loop.
async function executeTool(name, input) {
try {
return await callBackend(name, input);
} catch (err) {
if (err.code === "ETIMEDOUT" || err.status === 429) {
await sleep(500);
return await callBackend(name, input); // one silent retry
}
throw err; // let it surface as a tool_result error
}
}
Where SubToAPI fits
If you're running Claude's tool use through SubToAPI, the request/response shape for tool calls and results is the same as calling Claude directly — see the tool use docs and messages reference. The practical benefit is visibility: usage metadata per request lets you track which tools are triggering repeated errors or retries across your team, instead of digging through scattered logs. Get a sub_live_... key at signup and check the quickstart to wire it into an existing tool-calling loop in minutes.
Questions
What does is_error: true actually do in a tool_result? It signals to Claude that the tool call failed, prompting it to revise arguments or try an alternative approach, rather than treating the content as a normal successful result to reason over.
How many retries should I allow before giving up? Two to three is typical. More than that usually means the error is permanent (bad permissions, missing data) and no amount of rephrased arguments will fix it — return a clear "stop retrying" message instead.
Should I log tool errors separately from execution errors? Yes. Validation errors (bad arguments) and execution errors (backend failures) have different root causes and fixes — logging them together makes it hard to tell if the problem is your schema or your infrastructure.