CCA-F Study Day 7/20: Structured Error Responses
Domain 2: Tool Design & MCP Integration (~18-20% of exam)
📌 Today's Focus
Yesterday you learned how tool descriptions drive Claude's tool selection. Today we go deeper into what happens after a tool executes — specifically, what to do when things go wrong. Structured error responses are one of the exam's favorite testing grounds because the anti-patterns are intuitive-sounding but architecturally wrong. The exam will absolutely present scenarios where a tool fails and ask you what the correct response format should be.
This matters in production because Claude uses error context to decide its next action: retry, escalate, try an alternate path, or inform the user. If your error responses are generic or missing key metadata, Claude is flying blind.
🧠 Core Concepts
1. The is_error Flag
When returning a tool_result to the Messages API, you have an explicit is_error field. This is a boolean that tells Claude the tool execution failed. This is NOT optional for failures.
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "...", // error details go here
"is_error": true // CRITICAL: tells Claude this failed
}
Why it matters: Without is_error: true, Claude interprets whatever content you return as a successful result. If your tool returns "No records found" without the flag, Claude thinks it successfully found zero records — not that it failed to search.
2. Required Error Fields (The Exam Loves These)
A well-structured error response should include:
| Field | Type | Purpose |
|---|---|---|
is_error |
boolean | Signal to Claude that the tool failed (API level) |
errorCategory |
string | Classification of what went wrong |
isRetryable |
boolean | Can Claude try again, or is this permanent? |
context |
string | Human-readable explanation of the failure |
suggestion |
string | What Claude could do next |
3. Error Categories
The standard error categories you should know:
authentication— Credentials invalid/expired. NOT retryable (needs user action).authorization— User lacks permission. NOT retryable without escalation.validation— Input was malformed. IS retryable (with corrected input).rate_limit— Too many requests. IS retryable (after delay).not_found— Resource doesn't exist. NOT retryable (unless wrong ID).timeout— Operation took too long. IS retryable.server_error— Backend failure. IS retryable.
4. Distinguishing "No Data" from "Failed to Access"
This is a critical exam concept. These two situations look similar but mean very different things:
| Situation | Correct Response | is_error |
|---|---|---|
| Database queried successfully, zero results matched | {"results": [], "count": 0, "query_executed": true} |
false |
| Database connection failed, couldn't run query | {"errorCategory": "server_error", "isRetryable": true, ...} |
true |
The first is a successful operation that returned empty results. The second is a failure. Never conflate them.
5. How Claude Uses Error Metadata for Decision-Making
When Claude receives a structured error, it uses the metadata to decide its next action:
isRetryable: true→ Claude may retry the same tool call (possibly with modified params)isRetryable: false+errorCategory: "authentication"→ Claude asks the user to re-authenticateerrorCategory: "validation"+suggestion→ Claude modifies its input based on the suggestionerrorCategory: "not_found"→ Claude may try an alternate lookup strategy
🚫 Anti-Patterns & Exam Traps
Trap 1: Generic Error Messages
// ❌ WRONG — The exam WILL present this as an option
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "Operation failed",
"is_error": true
}
// ✅ CORRECT — Rich, actionable error
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "{\"error\": \"Too many attendees (max 10)\", \"errorCategory\": \"validation\", \"isRetryable\": true, \"suggestion\": \"Split into multiple events or remove some attendees\", \"currentCount\": 15, \"maxAllowed\": 10}",
"is_error": true
}
Why it's wrong: "Operation failed" gives Claude zero context to decide what to do next. It can't retry intelligently, can't suggest alternatives, can't inform the user of the specific issue.
Trap 2: Silently Suppressing Errors
// ❌ WRONG — Returning empty results as "success"
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "[]"
// Notice: NO is_error flag! Claude thinks this is a valid empty result
}
// ✅ CORRECT — Clearly signal the failure
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "{\"errorCategory\": \"server_error\", \"isRetryable\": true, \"context\": \"Database connection timed out after 30s\"}",
"is_error": true
}
Why it's wrong: Claude will tell the user "No records found" when in reality the search never executed. This is a data integrity disaster in production.
Trap 3: Missing isRetryable Field
The exam may present an error response that has good context but is missing the retryability signal. Without isRetryable, Claude has to guess whether to retry — and guessing is never the right architectural pattern.
Trap 4: Using HTTP Status Codes as Error Categories
Don't return "error": "404". Errors should be in semantic categories that Claude can reason about, not HTTP implementation details.
💻 Code Examples
Production-Ready Error Response Pattern
import json
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class ErrorCategory(Enum):
AUTHENTICATION = "authentication"
AUTHORIZATION = "authorization"
VALIDATION = "validation"
RATE_LIMIT = "rate_limit"
NOT_FOUND = "not_found"
TIMEOUT = "timeout"
SERVER_ERROR = "server_error"
@dataclass
class ToolError:
category: ErrorCategory
is_retryable: bool
context: str
suggestion: Optional[str] = None
metadata: Optional[dict] = None
def to_tool_result(self, tool_use_id: str) -> dict:
error_content = {
"errorCategory": self.category.value,
"isRetryable": self.is_retryable,
"context": self.context,
}
if self.suggestion:
error_content["suggestion"] = self.suggestion
if self.metadata:
error_content.update(self.metadata)
return {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(error_content),
"is_error": True
}
# Usage examples for each error category:
def handle_db_timeout(tool_use_id: str) -> dict:
return ToolError(
category=ErrorCategory.TIMEOUT,
is_retryable=True,
context="Database query exceeded 30s timeout",
suggestion="Try with a narrower date range or fewer filters"
).to_tool_result(tool_use_id)
def handle_auth_expired(tool_use_id: str) -> dict:
return ToolError(
category=ErrorCategory.AUTHENTICATION,
is_retryable=False,
context="OAuth token expired at 2026-05-22T06:00:00Z",
suggestion="Ask the user to re-authenticate via the settings page"
).to_tool_result(tool_use_id)
def handle_validation_error(tool_use_id: str, field: str, constraint: str) -> dict:
return ToolError(
category=ErrorCategory.VALIDATION,
is_retryable=True,
context=f"Field '{field}' failed validation: {constraint}",
suggestion=f"Correct the '{field}' value and retry",
metadata={"invalid_field": field, "constraint": constraint}
).to_tool_result(tool_use_id)
Complete Agentic Loop with Error Handling
import json
import anthropic
client = anthropic.Anthropic()
def execute_tool(name: str, input_data: dict, tool_use_id: str) -> dict:
"""Execute a tool and return a properly structured result."""
try:
result = call_external_service(name, input_data)
# Success case — note: NO is_error field needed
return {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(result)
}
except AuthenticationError as e:
return ToolError(
category=ErrorCategory.AUTHENTICATION,
is_retryable=False,
context=str(e),
suggestion="User must re-authenticate"
).to_tool_result(tool_use_id)
except RateLimitError as e:
return ToolError(
category=ErrorCategory.RATE_LIMIT,
is_retryable=True,
context=f"Rate limited. Retry after {e.retry_after}s",
metadata={"retry_after_seconds": e.retry_after}
).to_tool_result(tool_use_id)
except TimeoutError as e:
return ToolError(
category=ErrorCategory.TIMEOUT,
is_retryable=True,
context=f"Operation timed out after {e.timeout}s",
suggestion="Try with fewer parameters or a smaller scope"
).to_tool_result(tool_use_id)
except NotFoundError as e:
return ToolError(
category=ErrorCategory.NOT_FOUND,
is_retryable=False,
context=f"Resource not found: {e.resource_id}",
suggestion="Verify the ID is correct or search by name instead"
).to_tool_result(tool_use_id)
# The agentic loop incorporating structured errors
messages = [{"role": "user", "content": "Look up order ORD-99999"}]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input, block.id)
tool_results.append(result)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
🎬 Video to Watch
Building Effective AI Agents — Anthropic Webinar
This official Anthropic webinar covers single-agent and multi-agent designs with real examples from Coinbase, Intercom, and Thomson Reuters. Pay particular attention to the section on error handling patterns and how production agents recover from tool failures. The discussion of error propagation across agent boundaries directly relates to today's topic.
📖 Reading
- Primary: Writing Effective Tools for AI Agents — Anthropic Engineering Blog — Focus on the "Returning meaningful context from your tools" section. This directly covers how to make tool responses (including errors) useful to agents.
- Reference: How to implement tool use — Anthropic Docs — The official implementation guide including error response format.
🛠️ Hands-On Exercise (20 minutes)
Build a complete error handling framework for a 4-tool customer support toolkit:
- Define 5 error scenarios for each of these tools:
lookup_order,issue_refund,check_inventory,escalate_to_human - Write the structured error response for each scenario with all required fields
- Test it: Build a mock version where you randomly inject failures and verify Claude handles each error category differently
Your error scenarios should include:
lookup_order: order not found, database timeout, auth expiredissue_refund: amount exceeds policy limit (validation), payment processor down (server_error)check_inventory: warehouse system rate limited, invalid SKU formatescalate_to_human: no agents available (retryable), escalation queue full
📝 Quick Quiz
Question 1: A tool that searches a customer database returns an empty array [] because the database connection timed out. What is the correct response?
A) Return {"results": [], "count": 0} — empty results are valid
B) Return {"errorCategory": "timeout", "isRetryable": true, "context": "Database connection timed out"} with is_error: true
C) Return "Error: timeout" with is_error: true
D) Retry automatically 3 times before returning anything to Claude
Question 2: An agent's tool encounters an expired API key. Which error response field combination is MOST important for Claude to handle this correctly?
A) errorCategory: "server_error", isRetryable: true
B) errorCategory: "authentication", isRetryable: false, suggestion: "Ask user to re-authenticate"
C) errorCategory: "authentication", isRetryable: true
D) error: "401 Unauthorized"
Question 3: You're designing a tool that checks inventory. When the warehouse API returns HTTP 429 (rate limited), what should your tool result include?
A) is_error: false with {"inventory": "unavailable"}
B) is_error: true with {"errorCategory": "rate_limit", "isRetryable": true, "context": "Warehouse API rate limit hit", "metadata": {"retry_after_seconds": 30}}
C) is_error: true with "Rate limited"
D) Throw an exception and let the SDK handle it
Answers
1: B — A timeout is a failure, not an empty result. You must signal it with is_error: true and include category + retryability. Option C is wrong because it's a generic error string lacking actionable metadata.
2: B — Authentication failures are NOT retryable (retrying with the same expired key will always fail). The suggestion field tells Claude what recovery action to take. Option C is wrong because marking auth errors as retryable causes wasteful loops.
3: B — Rate limits ARE retryable, and including retry_after_seconds in metadata gives Claude (or the orchestration layer) precise timing for the retry. Option A is wrong because suppressing the error means Claude thinks inventory is legitimately unavailable.
👀 Tomorrow's Preview
Day 8: MCP Architecture & Protocol — We'll dive into the Model Context Protocol's three-layer architecture (Host → Client → Server), the two transport layers (stdio vs HTTP+SSE), and the three primitives (Tools, Resources, Prompts). This is the open standard that underpins all modern tool integration with Claude.