Handling Tool Errors Gracefully: Structured Resilient Conventions
Why tools in agentic AI systems must never raise raw exceptions, and how to implement a structured error convention that lets models reason about failures and self-heal.

TL;DR: In traditional software, functions raise exceptions when something goes wrong. In an AI agent loop, an uncaught exception instantly crashes the entire conversation. In this post, we establish the Structured Error Convention: tools never throw; instead, they return structured diagnostic dictionaries so the model can reason about the failure, ask the user for missing details, or try alternative recovery paths.
📦 Interactive Notebook: Test and run this code live on GitHub:
10_error_handling.ipynbin the ai-architect-foundation-gemini repository.
What You'll Learn
Why exceptions break agent loops, how to design a standardized 3-shape tool response schema, how to categorize tool errors (user_error, system_error, policy_violation), and how to build self-healing agent workflows.
The Fatal Flaw of Exceptions in Agents
Consider what happens when a tool raises a standard Python ValueError:
# ❌ Fatal Pattern: Raising exceptions inside tools
def lookup_order(order_id: str):
if not order_id.startswith("ORD-"):
raise ValueError(f"Invalid order ID format: {order_id}")
...When an agent invokes this tool during turn 3 of an interactive troubleshooting session:
- The Python interpreter terminates with an unhandled exception.
- The entire conversation context is aborted.
- The user sees a generic
"Internal Server Error"screen.
The agent never gets the opportunity to say: "It looks like you gave me tracking number 12345 instead of your order ID (which starts with ORD-). Could you check your receipt?"
The Structured Tool Response Convention
Every tool in a production agentic system should return a dictionary conforming to one of three standardized shapes:
| Result Shape | Key Indicator | When to Use |
|---|---|---|
| Success | {"status": "success", ...} | The operation succeeded with valid data. |
| User Input Error | {"status": "error", "error_category": "user_error", ...} | Invalid format, missing required parameter, order not found. |
| System / Transient Error | {"status": "error", "error_category": "system_error", ...} | Database connection timeout, 3rd party API rate limit. |
| Policy Rejection | {"status": "error", "error_category": "policy_rejection", ...} | Return requested past 30-day limit, unverified customer. |
Implementing Resilient Tools
from google import genai
from google.genai import types
client = genai.Client()
model = "gemini-2.5-flash"
# Resilient Tool Implementation
def lookup_order(order_id: str) -> dict:
# 1. Format validation check
if not order_id.startswith("ORD-"):
return {
"status": "error",
"error_category": "user_error",
"error_code": "INVALID_ORDER_FORMAT",
"message": f"Order ID '{order_id}' is invalid. Order IDs must start with 'ORD-' followed by numbers (e.g. ORD-1001).",
"suggested_action": "Ask the customer to check their order confirmation email for an ID starting with ORD-."
}
# 2. Simulated Database Lookup
db = {
"ORD-1001": {"item": "Leather Jacket", "delivered_days_ago": 12, "refundable": True},
"ORD-2002": {"item": "Wool Sweater", "delivered_days_ago": 45, "refundable": False}
}
if order_id not in db:
return {
"status": "error",
"error_category": "user_error",
"error_code": "ORDER_NOT_FOUND",
"message": f"No order found matching ID '{order_id}'.",
"suggested_action": "Verify if the customer has an alternate order ID or email address."
}
order_data = db[order_id]
return {
"status": "success",
"data": order_data
}Observing Self-Healing Agent Behavior
Let's test our agent with an invalid order ID:
# System prompt guiding error interpretation
system_prompt = """
You are EcommAssist AI. You help customers with order inquiries.
When tools return errors:
- If it's a user_error or INVALID_ORDER_FORMAT, explain the issue politely to the customer and ask for the correct information.
- If it's an ORDER_NOT_FOUND, offer to look up by another method.
- Never show raw JSON error codes to the customer.
"""
# Tool declaration
tool_decl = types.FunctionDeclaration(
name="lookup_order",
description="Fetch order status and item details.",
parameters=types.Schema(
type="OBJECT",
properties={"order_id": types.Schema(type="STRING")},
required=["order_id"]
)
)
tools = [types.Tool(function_declarations=[tool_decl])]
tool_map = {"lookup_order": lookup_order}
import json
result = lookup_order("98765")
print(json.dumps(result, indent=2))
# --- Execution Output ---
# {
# "status": "error",
# "error_category": "user_error",
# "error_code": "INVALID_ORDER_FORMAT",
# "message": "Order ID '98765' is invalid. Order IDs must begin with 'ORD-'.",
# "suggested_action": "Ask the customer to check their order confirmation email for an ID starting with ORD-."
# }When the customer says: "Check my order 98765", the tool returns the INVALID_ORDER_FORMAT error dictionary.
The model reads this in its next turn and gracefully responds:
"Thank you for reaching out! It looks like '98765' is missing the standard 'ORD-' prefix. Could you please double-check your receipt or confirmation email and provide the full order number (e.g., ORD-98765)?"
Handling Transient System Failures
For downstream network blunders or database outages:
def process_payment(amount: float) -> dict:
try:
# Simulated transient gateway timeout
raise TimeoutError("Payment gateway timed out after 5000ms")
except Exception as e:
return {
"status": "error",
"error_category": "system_error",
"error_code": "GATEWAY_TIMEOUT",
"retryable": True,
"message": "The payment gateway is temporarily unreachable. Do not charge the customer again immediately.",
"suggested_action": "Inform the customer that the payment system is experiencing a temporary delay and their card has not been charged."
}Because the error dictionary contains retryable: True and explicit instructions, the model will not panic or tell the customer their card was declined.
Key Takeaways
- Tools Never Throw: Wrap all tool bodies in comprehensive
try/exceptblocks that return structured status objects. - Include Guidance Fields: Provide
suggested_actionanderror_categoryso the model knows what conversational move to make next. - Graceful User Recovery: Empower models to explain formatting issues and solicit corrections naturally.
What's Next
In Part 11: Designing a Production-Ready Toolset for AI Agents, we look at organizing full enterprise toolkits, enforcing prerequisite sequencing, and separating read policies from write actions.