Files
gh-raw-labs-claude-code-mar…/skills/mxcp-expert/references/error-handling-guide.md
2025-11-30 08:49:50 +08:00

17 KiB

Error Handling Guide

Comprehensive error handling for MXCP servers: SQL errors (managed by MXCP) and Python errors (YOU must handle).

Two Types of Error Handling

1. SQL Errors (Managed by MXCP)

MXCP automatically handles:

  • SQL syntax errors
  • Type mismatches
  • Parameter binding errors
  • Database connection errors

Your responsibility:

  • Write correct SQL
  • Use proper parameter binding ($param)
  • Match return types to actual data

2. Python Errors (YOU Must Handle)

You MUST handle:

  • External API failures
  • Invalid input
  • Resource not found
  • Business logic errors
  • Async/await errors

Return structured error objects, don't raise exceptions to MXCP.

Python Error Handling Pattern

WRONG: Let Exceptions Bubble Up

# python/api_wrapper.py
async def fetch_user(user_id: int) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.example.com/users/{user_id}")
        response.raise_for_status()  # ❌ Will crash if 404/500!
        return response.json()

Problem: When API returns 404, exception crashes the tool. LLM gets unhelpful error.

CORRECT: Return Structured Errors

# python/api_wrapper.py
import httpx

async def fetch_user(user_id: int) -> dict:
    """
    Fetch user from external API.

    Returns:
        Success: {"success": true, "user": {...}}
        Error: {"success": false, "error": "User not found", "error_code": "NOT_FOUND"}
    """
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            response = await client.get(
                f"https://api.example.com/users/{user_id}"
            )

            if response.status_code == 404:
                return {
                    "success": False,
                    "error": f"User with ID {user_id} not found",
                    "error_code": "NOT_FOUND",
                    "user_id": user_id
                }

            if response.status_code >= 500:
                return {
                    "success": False,
                    "error": "External API is currently unavailable. Please try again later.",
                    "error_code": "API_ERROR",
                    "status_code": response.status_code
                }

            response.raise_for_status()  # Other HTTP errors

            return {
                "success": True,
                "user": response.json()
            }

    except httpx.TimeoutException:
        return {
            "success": False,
            "error": "Request timed out after 10 seconds. The API may be slow or unavailable.",
            "error_code": "TIMEOUT"
        }

    except httpx.HTTPError as e:
        return {
            "success": False,
            "error": f"HTTP error occurred: {str(e)}",
            "error_code": "HTTP_ERROR"
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Unexpected error: {str(e)}",
            "error_code": "UNKNOWN_ERROR"
        }

Why good:

  • LLM gets clear error message
  • LLM knows what went wrong (error_code)
  • LLM can take action (retry, try different ID, etc.)
  • Tool never crashes

Error Response Structure

Standard Error Format

{
    "success": False,
    "error": "Human-readable error message for LLM",
    "error_code": "MACHINE_READABLE_CODE",
    "details": {  # Optional: additional context
        "attempted_value": user_id,
        "valid_range": "1-1000"
    }
}

Standard Success Format

{
    "success": True,
    "data": {
        # Actual response data
    }
}

Common Error Scenarios

1. Input Validation Errors

def process_order(order_id: str, quantity: int) -> dict:
    """Process an order with validation"""

    # Validate order_id format
    if not order_id.startswith("ORD_"):
        return {
            "success": False,
            "error": f"Invalid order ID format. Expected format: 'ORD_XXXXX', got: '{order_id}'",
            "error_code": "INVALID_FORMAT",
            "expected_format": "ORD_XXXXX",
            "provided": order_id
        }

    # Validate quantity range
    if quantity <= 0:
        return {
            "success": False,
            "error": f"Quantity must be positive. Got: {quantity}",
            "error_code": "INVALID_QUANTITY",
            "provided": quantity,
            "valid_range": "1 or greater"
        }

    if quantity > 1000:
        return {
            "success": False,
            "error": f"Quantity {quantity} exceeds maximum allowed (1000). Please split into multiple orders.",
            "error_code": "QUANTITY_EXCEEDED",
            "provided": quantity,
            "maximum": 1000
        }

    # Process order...
    return {"success": True, "order_id": order_id, "quantity": quantity}

2. Resource Not Found Errors

from mxcp.runtime import db

def get_customer(customer_id: str) -> dict:
    """Get customer by ID with proper error handling"""

    try:
        result = db.execute(
            "SELECT * FROM customers WHERE customer_id = $1",
            {"customer_id": customer_id}
        )

        customer = result.fetchone()

        if customer is None:
            return {
                "success": False,
                "error": f"Customer '{customer_id}' not found in database. Use list_customers to see available customers.",
                "error_code": "CUSTOMER_NOT_FOUND",
                "customer_id": customer_id,
                "suggestion": "Call list_customers tool to see all available customer IDs"
            }

        return {
            "success": True,
            "customer": dict(customer)
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Database error while fetching customer: {str(e)}",
            "error_code": "DATABASE_ERROR"
        }

3. External API Errors

import httpx

async def create_customer_in_stripe(email: str, name: str) -> dict:
    """Create Stripe customer with comprehensive error handling"""

    try:
        import stripe
        from mxcp.runtime import get_secret

        # Get API key
        secret = get_secret("stripe")
        if not secret:
            return {
                "success": False,
                "error": "Stripe API key not configured. Please set up 'stripe' secret in config.yml",
                "error_code": "MISSING_CREDENTIALS",
                "required_secret": "stripe"
            }

        stripe.api_key = secret.get("api_key")

        # Create customer
        customer = stripe.Customer.create(
            email=email,
            name=name
        )

        return {
            "success": True,
            "customer_id": customer.id,
            "email": customer.email
        }

    except stripe.error.InvalidRequestError as e:
        return {
            "success": False,
            "error": f"Invalid request to Stripe: {str(e)}",
            "error_code": "INVALID_REQUEST",
            "details": str(e)
        }

    except stripe.error.AuthenticationError:
        return {
            "success": False,
            "error": "Stripe API key is invalid or expired. Please update credentials.",
            "error_code": "AUTHENTICATION_FAILED"
        }

    except stripe.error.RateLimitError:
        return {
            "success": False,
            "error": "Stripe rate limit exceeded. Please try again in a few seconds.",
            "error_code": "RATE_LIMIT",
            "suggestion": "Wait 5-10 seconds and retry"
        }

    except stripe.error.StripeError as e:
        return {
            "success": False,
            "error": f"Stripe error: {str(e)}",
            "error_code": "STRIPE_ERROR"
        }

    except ImportError:
        return {
            "success": False,
            "error": "Stripe library not installed. Run: pip install stripe",
            "error_code": "MISSING_DEPENDENCY",
            "fix": "pip install stripe>=5.0.0"
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Unexpected error: {str(e)}",
            "error_code": "UNKNOWN_ERROR"
        }

4. Business Logic Errors

def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
    """Transfer funds with business logic validation"""

    # Check amount
    if amount <= 0:
        return {
            "success": False,
            "error": f"Transfer amount must be positive. Got: ${amount}",
            "error_code": "INVALID_AMOUNT"
        }

    # Check account exists and get balance
    from_balance = db.execute(
        "SELECT balance FROM accounts WHERE account_id = $1",
        {"account_id": from_account}
    ).fetchone()

    if from_balance is None:
        return {
            "success": False,
            "error": f"Source account '{from_account}' not found",
            "error_code": "ACCOUNT_NOT_FOUND",
            "account_id": from_account
        }

    # Check sufficient funds
    if from_balance["balance"] < amount:
        return {
            "success": False,
            "error": f"Insufficient funds. Available: ${from_balance['balance']:.2f}, Requested: ${amount:.2f}",
            "error_code": "INSUFFICIENT_FUNDS",
            "available": from_balance["balance"],
            "requested": amount,
            "shortfall": amount - from_balance["balance"]
        }

    # Perform transfer...
    return {
        "success": True,
        "transfer_id": "TXN_12345",
        "from_account": from_account,
        "to_account": to_account,
        "amount": amount
    }

5. Async/Await Errors

import asyncio

async def fetch_multiple_users(user_ids: list[int]) -> dict:
    """Fetch multiple users concurrently with error handling"""

    async def fetch_one(user_id: int) -> dict:
        try:
            async with httpx.AsyncClient(timeout=5.0) as client:
                response = await client.get(f"https://api.example.com/users/{user_id}")

                if response.status_code == 404:
                    return {
                        "user_id": user_id,
                        "success": False,
                        "error": f"User {user_id} not found"
                    }

                response.raise_for_status()
                return {
                    "user_id": user_id,
                    "success": True,
                    "user": response.json()
                }

        except asyncio.TimeoutError:
            return {
                "user_id": user_id,
                "success": False,
                "error": f"Timeout fetching user {user_id}"
            }

        except Exception as e:
            return {
                "user_id": user_id,
                "success": False,
                "error": str(e)
            }

    # Fetch all concurrently
    results = await asyncio.gather(*[fetch_one(uid) for uid in user_ids])

    # Separate successes and failures
    successes = [r for r in results if r["success"]]
    failures = [r for r in results if not r["success"]]

    return {
        "success": len(failures) == 0,
        "total_requested": len(user_ids),
        "successful": len(successes),
        "failed": len(failures),
        "users": [r["user"] for r in successes],
        "errors": [{"user_id": r["user_id"], "error": r["error"]} for r in failures]
    }

Error Messages for LLMs

Principles for Good Error Messages

  1. Be Specific: Tell exactly what went wrong
  2. Be Actionable: Suggest what to do next
  3. Provide Context: Include relevant values/IDs
  4. Use Plain Language: Avoid technical jargon

BAD Error Messages

return {"error": "Error"}  # ❌ Useless
return {"error": "Invalid input"}  # ❌ Which input? Why invalid?
return {"error": "DB error"}  # ❌ What kind of error?
return {"error": str(e)}  # ❌ Raw exception message (often cryptic)

GOOD Error Messages

return {
    "error": "Customer ID 'CUST_999' not found. Use list_customers to see available IDs."
}

return {
    "error": "Date format invalid. Expected 'YYYY-MM-DD' (e.g., '2024-01-15'), got: '01/15/2024'"
}

return {
    "error": "Quantity 5000 exceeds maximum allowed (1000). Split into multiple orders or contact support."
}

return {
    "error": "API rate limit exceeded. Please wait 30 seconds and try again."
}

SQL Error Handling (MXCP Managed)

You Don't Handle These (MXCP Does)

MXCP automatically handles and returns errors for:

  • Invalid SQL syntax
  • Missing tables/columns
  • Type mismatches
  • Parameter binding errors

Your job: Write correct SQL and let MXCP handle errors.

Prevent SQL Errors

1. Validate Schema

# Always define return types to match SQL output
tool:
  name: get_stats
  return:
    type: object
    properties:
      total: { type: number }  # Matches SQL: SUM(amount)
      count: { type: integer }  # Matches SQL: COUNT(*)
  source:
    code: |
      SELECT
        SUM(amount) as total,
        COUNT(*) as count
      FROM orders

2. Handle NULL Values

-- BAD: Might return NULL which breaks type system
SELECT amount FROM orders WHERE id = $order_id

-- GOOD: Handle potential NULL
SELECT COALESCE(amount, 0) as amount
FROM orders
WHERE id = $order_id

-- GOOD: Use IFNULL/COALESCE for aggregations
SELECT
  COALESCE(SUM(amount), 0) as total,
  COALESCE(AVG(amount), 0) as average
FROM orders
WHERE status = $status

3. Handle Empty Results

-- If no results, return empty array (not NULL)
SELECT * FROM customers WHERE city = $city
-- Returns: [] if no customers (MXCP handles this)

-- For aggregations, always return a row
SELECT
  COUNT(*) as count,
  COALESCE(SUM(amount), 0) as total
FROM orders
WHERE status = $status
-- Always returns one row, even if no matching orders

Error Codes Convention

Use consistent error codes across your tools:

# Standard error codes
ERROR_CODES = {
    # Input validation
    "INVALID_FORMAT": "Input format is incorrect",
    "INVALID_RANGE": "Value outside valid range",
    "MISSING_REQUIRED": "Required parameter missing",

    # Resource errors
    "NOT_FOUND": "Resource not found",
    "ALREADY_EXISTS": "Resource already exists",
    "DELETED": "Resource has been deleted",

    # Permission errors
    "UNAUTHORIZED": "User not authenticated",
    "FORBIDDEN": "User lacks permission",

    # External service errors
    "API_ERROR": "External API error",
    "TIMEOUT": "Request timed out",
    "RATE_LIMIT": "Rate limit exceeded",

    # System errors
    "DATABASE_ERROR": "Database operation failed",
    "CONFIGURATION_ERROR": "Missing or invalid configuration",
    "DEPENDENCY_ERROR": "Required library not installed",

    # Business logic errors
    "INSUFFICIENT_FUNDS": "Not enough balance",
    "INVALID_STATE": "Operation not allowed in current state",
    "QUOTA_EXCEEDED": "Usage quota exceeded",

    # Unknown
    "UNKNOWN_ERROR": "Unexpected error occurred"
}

Testing Error Handling

Unit Tests for Error Cases

# tests/test_error_handling.py
import pytest
from python.my_module import fetch_user

@pytest.mark.asyncio
async def test_fetch_user_not_found(httpx_mock):
    """Test 404 error handling"""
    httpx_mock.add_response(
        url="https://api.example.com/users/999",
        status_code=404
    )

    result = await fetch_user(999)

    assert result["success"] is False
    assert result["error_code"] == "NOT_FOUND"
    assert "999" in result["error"]  # Error mentions the ID

@pytest.mark.asyncio
async def test_fetch_user_timeout(httpx_mock):
    """Test timeout handling"""
    httpx_mock.add_exception(httpx.TimeoutException("Timeout"))

    result = await fetch_user(123)

    assert result["success"] is False
    assert result["error_code"] == "TIMEOUT"
    assert "timeout" in result["error"].lower()

def test_invalid_input():
    """Test input validation"""
    result = process_order("INVALID", quantity=5)

    assert result["success"] is False
    assert result["error_code"] == "INVALID_FORMAT"
    assert "ORD_" in result["error"]  # Mentions expected format

Error Handling Checklist

Before declaring Python tool complete:

  • All external API calls wrapped in try/except
  • All exceptions return structured error objects
  • Error messages are clear and actionable
  • Error codes are consistent
  • Input validation with helpful error messages
  • NULL/None values handled gracefully
  • Timeout handling for network calls
  • Missing dependencies handled (ImportError)
  • Database errors caught and explained
  • Success/failure clearly indicated in response
  • Unit tests for error scenarios
  • Error messages help LLM understand what to do next

Summary

SQL Tools (MXCP Handles):

  • Write correct SQL
  • Handle NULL values with COALESCE
  • Match return types to SQL output

Python Tools (YOU Handle):

  • Wrap ALL external calls in try/except
  • Return structured error objects ({"success": False, "error": "...", "error_code": "..."})
  • Validate inputs with clear error messages
  • Be specific and actionable in error messages
  • Use consistent error codes
  • Test error scenarios
  • NEVER let exceptions bubble up to MXCP

Golden Rule: Errors should help the LLM understand what went wrong and what to do next.