Pattern-014: Error Handling Pattern (API Contract)

Status

Proven

Context

Web APIs need consistent, actionable error responses that follow RESTful principles and give users clear guidance on resolution. Without systematic error handling, applications provide inconsistent error formats, expose internal implementation details, and frustrate users with unhelpful error messages. The Error Handling Pattern addresses:

Pattern Description

The Error Handling Pattern creates centralized error handling that converts domain exceptions into consistent API responses with both technical details for debugging and user-friendly messages for resolution guidance.

Core concept:

Implementation

Centralized Error Handler

from fastapi import JSONResponse
from typing import Dict, Type

class APIErrorHandler:
    """Centralized error handling for consistent responses"""

    ERROR_MESSAGES = {
        ProjectNotFoundError: "I couldn't find that project. Try 'list projects' to see available options.",
        AmbiguousProjectError: "Multiple projects match your request. Please be more specific.",
        GitHubAPIError: "GitHub is temporarily unavailable. Please try again in a few moments.",
        InsufficientContextError: "I need more information to complete this task. {details}"
    }

    @staticmethod
    def handle_error(error: Exception) -> JSONResponse:
        """Convert exceptions to user-friendly API responses"""
        if isinstance(error, ProjectNotFoundError):
            return JSONResponse(
                status_code=404,
                content={
                    "status": "error",
                    "error": {
                        "code": "PROJECT_NOT_FOUND",
                        "message": "The specified project does not exist",
                        "user_message": APIErrorHandler.ERROR_MESSAGES[ProjectNotFoundError],
                        "details": {"project_id": error.project_id}
                    }
                }
            )
        elif isinstance(error, ValidationError):
            return JSONResponse(
                status_code=422,
                content={
                    "status": "error",
                    "error": {
                        "code": "VALIDATION_ERROR",
                        "message": str(error),
                        "field": getattr(error, 'field', None)
                    }
                }
            )
        else:
            # Generic error with safe message
            return JSONResponse(
                status_code=500,
                content={
                    "status": "error",
                    "error": {
                        "code": "INTERNAL_ERROR",
                        "message": "An unexpected error occurred",
                        "user_message": "Something went wrong. Please try again or contact support."
                    }
                }
            )

API Endpoint Integration

from fastapi import FastAPI

app = FastAPI()

@app.post("/api/v1/intent")
async def process_intent(request: IntentRequest):
    try:
        result = await intent_service.process(request)
        return JSONResponse(status_code=200, content=result)
    except Exception as e:
        return APIErrorHandler.handle_error(e)

@app.get("/api/v1/projects/{project_id}")
async def get_project(project_id: str):
    try:
        project = await project_service.get_by_id(project_id)
        return {"status": "success", "data": project}
    except Exception as e:
        return APIErrorHandler.handle_error(e)

Domain Exception Classes

class DomainException(Exception):
    """Base class for domain-specific exceptions"""

    def __init__(self, message: str, details: Dict = None):
        self.message = message
        self.details = details or {}
        super().__init__(message)

class ProjectNotFoundError(DomainException):
    """Raised when project cannot be found"""

    def __init__(self, project_id: str):
        self.project_id = project_id
        super().__init__(f"Project {project_id} not found", {"project_id": project_id})

class AmbiguousProjectError(DomainException):
    """Raised when multiple projects match criteria"""

    def __init__(self, criteria: str, matches: List[str]):
        self.criteria = criteria
        self.matches = matches
        super().__init__(f"Multiple projects match '{criteria}'", {
            "criteria": criteria,
            "matches": matches
        })

class ValidationError(DomainException):
    """Raised for business rule validation failures"""

    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(message, {"field": field})

Usage Guidelines

Error Response Contract

{
  "status": "error",
  "error": {
    "code": "ERROR_CODE",
    "message": "Technical error message",
    "user_message": "User-friendly guidance",
    "field": "field_name (if applicable)",
    "details": {
      /* optional context */
    }
  }
}

Status Code Guidelines

Code Usage Example
200 Success Request completed successfully
400 Bad Request Invalid JSON format or missing required fields
404 Not Found Project, workflow, or resource doesn’t exist
422 Validation Error Valid format but business rules violated
429 Rate Limited Too many requests from client
500 Server Error Unexpected internal error
502 Service Unavailable External service (GitHub, Claude) unavailable

User Message Guidelines

Error Handling Best Practices

Benefits

Trade-offs

Anti-patterns to Avoid

References

Migration Notes

Consolidated from: