AI & ML — ADVANCED
Tool Calling in Production: Errors, Structured Output, and a Real Dispatcher
Every example so far has assumed the tool call succeeds. Production systems have to handle the moment it doesn’t — and know when tool calling is even the right feature to reach for in the first place.
The Quick Answer
A failed tool call is reported back the same way a successful one is — as a tool_result block — just with is_error: true and a specific message describing what went wrong. Separately, it’s worth being precise about what tool calling actually solves: it’s for requesting real actions, not for controlling the shape of the model’s own final answer — that’s a distinct feature, structured output, and the two solve different problems.
Reporting a Failed Tool Call: is_error
When a tool call fails, the result still gets sent back as a tool_result — not silence, not a crash — with an extra field:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "ConnectionError: the weather service API is not available (HTTP 500)",
"is_error": true
}
]
}The model reads this exactly like any other result and decides what to do — often explaining the failure to the user (“I’m sorry, I was unable to retrieve the current weather because the service is unavailable”). The message itself matters: a specific, instructive error like “Rate limit exceeded. Retry after 60 seconds” gives the model something concrete to act on, where a bare “failed” doesn’t.
Tool execution error
The tool itself fails while running — a network error, a downstream API returning a 500. Return the real error in content with is_error: true, and the model can explain the failure to the user or suggest trying again.
Invalid tool call (missing or wrong arguments)
The model’s call is missing a required parameter or uses the wrong type — usually a sign the tool’s description didn’t give it enough to work with. Returning an is_error result describing exactly what’s missing lets the model retry with a correction, typically within 2–3 attempts, rather than failing outright.
Structured Output vs. Tool Calling: Not the Same Feature
Both features can guarantee a predictable JSON shape, which makes them easy to confuse. The actual distinction is about whatis being controlled: structured output controls the format of the model’s own final answer — useful for tasks like extracting fields from an invoice or classifying feedback, where the model’s reply isthe JSON. Tool calling is about requesting that the application execute a real action — the model isn’t producing its final answer, it’s asking for something to be done. The two aren’t mutually exclusive: a model can call a tool with strictly validated parameters and still return its eventual final answer as structured JSON.
| Aspect | Structured output | Tool calling |
|---|---|---|
| What it controls | The format of the model’s own final answer to the user. | A request for the application to execute a real action. |
| Who acts on the result | The application reads the JSON as the final answer — nothing further needs to run. | The application executes a real function, then reports the result back to the model. |
| Typical use case | Extracting invoice fields, classifying feedback, generating an API-ready summary. | Checking live weather, booking something, querying a database, calling an external API. |
| Is this the model’s last step? | Usually yes — the JSON returned is the final response. | No — a tool_result typically comes back, and the model continues from there. |
| Can they be combined? | Yes — a model can call a tool with validated parameters and still return its final answer as structured JSON. | Yes — same as the previous column, this is one combined workflow, not two separate features. |
A Real Multi-Tool Dispatcher, in Code
The Problem
Every example so far has handled exactly one tool, checked by hand. A real application usually registers several tools at once, and needs one consistent piece of code that: takes whatever tool_use block the model produced, figures out which real function that name corresponds to, runs it with the arguments the model supplied, and — critically — never lets a bad tool name or a function that throws an exception crash the whole request. Instead, any failure has to come back as a normal tool_result with is_error: true, exactly as covered above, so the model can react to it instead of the application simply dying. This single piece of routing logic is usually called a dispatcher.
The Code
def get_weather(location):
known = {'Tokyo, Japan': '21C, partly cloudy', 'Mumbai, India': '31C, humid'}
if location not in known:
raise ValueError(f'Unknown location: {location!r}')
return known[location]
def convert_currency(amount, from_currency, to_currency):
rates = {('USD', 'INR'): 83.2}
key = (from_currency, to_currency)
if key not in rates:
raise ValueError(f'No exchange rate available for {from_currency} to {to_currency}')
return round(amount * rates[key], 2)
TOOL_REGISTRY = {'get_weather': get_weather, 'convert_currency': convert_currency}
def dispatch_tool_call(tool_use_block):
name = tool_use_block['name']
tool_use_id = tool_use_block['id']
tool_fn = TOOL_REGISTRY.get(name)
if tool_fn is None:
return {'type': 'tool_result', 'tool_use_id': tool_use_id,
'content': f'Error: unknown tool {name!r}', 'is_error': True}
try:
result = tool_fn(**tool_use_block['input'])
return {'type': 'tool_result', 'tool_use_id': tool_use_id, 'content': str(result)}
except (TypeError, ValueError) as e:
return {'type': 'tool_result', 'tool_use_id': tool_use_id,
'content': f'Error: {e}', 'is_error': True}Line by Line: What Each Part Is Doing
get_weatherandconvert_currencyThe two real tool implementations — ordinary functions, with no awareness of the model at all. Each one deliberately
raises aValueErroron bad input (an unknown city, a currency pair with no rate) rather than returning something misleading likeNone— that’s what gives the dispatcher something concrete to catch.TOOL_REGISTRYA plain dictionary mapping each tool’s name — the exact string the model would use in a
tool_useblock’snamefield — to the actual function that implements it. This is the whole “registry” in tool registry: one lookup table, so adding a third tool later means adding one entry here, not more branching logic.name = tool_use_block['name']andtool_use_id = tool_use_block['id']Pulls the two fields out of the model’s tool call that matter here: which tool it wants, and the call’s unique id — needed later so the result can be matched back to this exact call, the same
tool_use_idmatching covered in the Intermediate level’s parallel tool calls.tool_fn = TOOL_REGISTRY.get(name)Looks up the function for that name. Using
.get()instead ofTOOL_REGISTRY[name]matters: a missing key returnsNoneinstead of raising aKeyError, which lets the very next line handle “unknown tool” deliberately, as a normal case, instead of an unhandled crash.if tool_fn is None: return {...}Handles the model naming a tool that was never registered at all — a different failure from a tool that runs but rejects its input. Returns immediately with
is_error: Trueand a message naming the unrecognized tool, without ever attempting to call anything.try: result = tool_fn(**tool_use_block['input'])The actual execution step.
**unpacks the model’sinputdictionary (like{'location': 'Tokyo, Japan'}) directly into the function’s named parameters, so the dispatcher doesn’t need to know each tool’s specific argument names — it works the same way for every tool in the registry. Wrapping this intryis what gives the next line something to catch.return {'type': 'tool_result', ...}(success path)On success, wraps the function’s return value as a normal
tool_result, tagged with the sametool_use_idpulled out two steps earlier — nois_errorfield at all, since it isn’t needed when nothing went wrong.except (TypeError, ValueError) as e:Catches exactly the two failure types the tool functions can realistically raise:
ValueErrorfor the deliberate rejections insideget_weatherandconvert_currency, andTypeErrorfor the case where the model’sinputis missing a required argument entirely, which Python raises on its own during the**unpacking above. Converts whichever one happened into the sameis_error: Trueshape as the unknown-tool case, so the model always receives one consistent error format no matter what actually broke.
Run against a real location, this correctly returns a plain tool_result with the weather. Run against location="Atlantis" or a currency pair with no known exchange rate, it correctly catches the error and returns is_error: True with a specific message — verified line for line, in both Python and Node, producing the same results either way.
Fun Fact
A registry like the one above is also exactly where the hallucinated-argument problem from AI Agent Advanced gets caught in practice — the tool function itself is the last line of defense, since it can reject an argument the model generated but that doesn’t actually correspond to anything real.
Test Yourself
What should the content of an is_error tool_result look like?
What happens when a model’s tool call is missing a required parameter?
What is the real difference between structured output and tool calling?
In the multi-tool dispatcher code on this page, what happens when an unknown location is passed to get_weather?