AI

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-authenticate
  • errorCategory: "validation" + suggestion → Claude modifies its input based on the suggestion
  • errorCategory: "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


🛠️ Hands-On Exercise (20 minutes)

Build a complete error handling framework for a 4-tool customer support toolkit:

  1. Define 5 error scenarios for each of these tools: lookup_orderissue_refundcheck_inventoryescalate_to_human
  2. Write the structured error response for each scenario with all required fields
  3. 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 expired
  • issue_refund: amount exceeds policy limit (validation), payment processor down (server_error)
  • check_inventory: warehouse system rate limited, invalid SKU format
  • escalate_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: falsesuggestion: "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.