Claude API Function Calling Best Practices
Function calling (Claude calls it "tool use") lets Claude invoke your code mid-conversation instead of just returning text. Getting it to work reliably in production is different from getting a demo to work once. The best practices below cover tool definition, execution flow, error handling, and validation — the parts that actually break when real users hit your app.
The short version: define tools with strict, narrow schemas; always validate Claude's input before executing anything; handle the full tool_use → tool_result loop explicitly; and treat tool errors as recoverable conversation turns, not exceptions that crash your request.
Design tools like you'd design a public API
Claude decides when and how to call a tool based entirely on the tool's name, description, and JSON schema. If that metadata is vague, Claude guesses — and guessing produces wrong arguments, wrong tool choices, or unnecessary calls.
- One tool, one responsibility. Don't build a
manage_usertool that creates, updates, and deletes based on a hiddenactionfield. Split it intocreate_user,update_user,delete_user. Claude picks between clearly named tools far more reliably than it infers an internal mode. - Write descriptions for the model, not your teammates. State what the tool does, when to use it, and what it returns. "Looks up current weather for a city. Returns temperature in Celsius and conditions." beats "Weather tool."
- Use enums and constrained types wherever possible. If a parameter only accepts
"celsius"or"fahrenheit", say so in the schema instead of accepting a free string. - Keep required fields minimal. Every required field is a chance for Claude to omit it and trigger a failed call. Make genuinely optional fields optional with sensible defaults documented in the description.
Handle the tool_use loop explicitly
A tool call isn't a single request — it's a conversation turn. Claude returns a stop_reason of tool_use along with one or more tool_use blocks. Your code executes the tool(s) and sends the results back as tool_result blocks in a new user message, continuing the same message history.
{
"role": "assistant",
"content": [
{ "type": "tool_use", "id": "toolu_01", "name": "get_order_status", "input": { "order_id": "A1234" } }
]
}
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "Order A1234: shipped, arriving Tuesday." }
]
}
Common mistakes here:
- Dropping the original assistant message. You must append Claude's
tool_usemessage to history before sending thetool_result— Claude needs to see its own call to interpret the result. - Mismatched
tool_use_id. Every result must reference the exact id from the call it answers. If Claude made three parallel calls, send three results with matching ids in one message. - Not looping. Claude may chain tool calls — call a tool, read the result, call another tool, then answer. Your code needs a loop that keeps calling the model until
stop_reasonisend_turn(or you hit a sane max-iteration cap).
Validate before you execute
Claude's tool arguments come from a JSON schema it tries to respect, but it is not a guarantee. Treat every tool_use.input like untrusted input from a web form:
- Validate types and required fields with a schema validator (Zod, Pydantic,
ajv) before touching your business logic. - Enforce authorization in your tool handler, not in the prompt. If a tool can delete a resource, check the authenticated user's permissions server-side — Claude's system prompt is not an access control layer.
- Clamp ranges and sanitize strings (file paths, SQL fragments, shell arguments) exactly as you would for any external API call. Function calling doesn't remove injection risk; it just moves the input source.
Handle tool errors as conversation, not crashes
When a tool fails — a timeout, a 404, invalid input — don't throw and end the request. Send the error back to Claude as a tool_result with is_error: true and a short, actionable message. Claude will usually retry with corrected arguments or explain the failure to the user instead of hallucinating a result.
{
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": "Order not found: A1234 does not exist.",
"is_error": true
}
This single pattern eliminates a large share of "Claude made up data" bug reports — most of them are actually unhandled tool failures that never reached the model.
Parallel tool calls and latency
Claude can request multiple tool calls in a single turn when they're independent (e.g., looking up weather for three cities). Execute these concurrently in your code — Promise.all in JavaScript, asyncio.gather in Python — rather than sequentially, and return all results together in one tool_result message. Sequential execution of independent calls is one of the most common unnecessary latency sources in tool-use apps.
Keep tool definitions stable across a conversation
Changing the tool list mid-conversation (adding/removing tools between turns) can confuse Claude about what's available. If you need conditional tools, decide the full set before the first message in a session and keep it consistent for the life of that conversation.
Monitor what's actually happening
Once tool use is in production, the failures that matter are the ones you can't see in a terminal: which tools get called most, which ones error out, and how much latency the tool loop adds per request. If you're running Claude behind a standard HTTPS API — like the one SubToAPI exposes over your existing Claude access — every tool-use request shows up with full metadata (tokens, latency, tool calls) in one dashboard, which makes it much faster to spot a tool that's silently failing 20% of the time. See the tool use docs and messages docs for request formats, or start with the quickstart.
Questions
Does Claude support parallel tool calls in one turn? Yes. Claude can return multiple tool_use blocks in a single response when the calls are independent. Execute them concurrently and return all corresponding tool_result blocks in one follow-up message.
What happens if Claude sends invalid arguments to my tool? Nothing stops it from happening — you must validate input yourself. If validation fails, return a tool_result with is_error: true describing the problem; Claude will typically retry with corrected arguments.
How many tools can I give Claude in one request? There's no hard cap on tool count, but accuracy drops as the tool list grows and tools overlap in purpose. Keep definitions narrow and distinct, and only include tools relevant to the current conversation.