AI Tools Compared

Claude Code helps developers implement consistent, user-friendly error handling across APIs. This guide covers the essential standards for designing error responses that improve debugging, enhance client experience, and maintain API reliability.

Why API Error Handling Matters

Effective error handling serves three critical purposes. First, it helps clients understand what went wrong and how to recover, reducing support burden. Second, it provides debugging information for developers during development and production. Third, it maintains API reliability by preventing cascading failures and providing clear status signals.

Poor error handling leads to frustrated users, difficult debugging sessions, and fragile integrations. By implementing standards from the start, you create APIs that are easier to maintain and consume.

HTTP Status Code Standards

Use HTTP status codes consistently to indicate the general category of the response.

Success Codes (2xx)

Client Error Codes (4xx)

Server Error Codes (5xx)

Error Response Format

Structure all error responses consistently. Use JSON for error bodies.

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested user does not exist",
    "details": {
      "resource_type": "user",
      "requested_id": "usr_12345"
    },
    "timestamp": "2026-03-17T10:30:00Z",
    "request_id": "req_abc123"
  }
}

Define standard error codes that your API uses:

# Example error code enum
class ErrorCode:
    # Authentication errors (AUTH_*)
    AUTH_INVALID_TOKEN = "AUTH_INVALID_TOKEN"
    AUTH_EXPIRED_TOKEN = "AUTH_EXPIRED_TOKEN"
    AUTH_MISSING_TOKEN = "AUTH_MISSING_TOKEN"

    # Validation errors (VALIDATION_*)
    VALIDATION_INVALID_FORMAT = "VALIDATION_INVALID_FORMAT"
    VALIDATION_MISSING_FIELD = "VALIDATION_MISSING_FIELD"
    VALIDATION_OUT_OF_RANGE = "VALIDATION_OUT_OF_RANGE"

    # Resource errors (RESOURCE_*)
    RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
    RESOURCE_CONFLICT = "RESOURCE_CONFLICT"
    RESOURCE_DELETED = "RESOURCE_DELETED"

    # Rate limiting (RATE_*)
    RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"

    # Server errors (INTERNAL_*)
    INTERNAL_ERROR = "INTERNAL_ERROR"
    SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"

Implementing Error Handling with Claude Code

Claude Code can help you implement strong error handling in multiple languages. Here is a Python FastAPI example:

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from enum import Enum
from datetime import datetime
import uuid

app = FastAPI()

class ErrorCode(str, Enum):
    RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
    VALIDATION_ERROR = "VALIDATION_ERROR"
    INTERNAL_ERROR = "INTERNAL_ERROR"

class APIException(Exception):
    def __init__(
        self,
        code: ErrorCode,
        message: str,
        details: dict = None,
        status_code: int = 400
    ):
        self.code = code
        self.message = message
        self.details = details or {}
        self.status_code = status_code
        super().__init__(message)

@app.exception_handler(APIException)
async def api_exception_handler(request: Request, exc: APIException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": {
                "code": exc.code.value,
                "message": exc.message,
                "details": exc.details,
                "timestamp": datetime.utcnow().isoformat() + "Z",
                "request_id": request.state.request_id
            }
        }
    )

@app.middleware("http")
async def add_request_id(request: Request, call_next):
    request.state.request_id = str(uuid.uuid4())
    response = await call_next(request)
    response.headers["X-Request-ID"] = request.state.request_id
    return response

Error Handling Best Practices

Always Include Request IDs

Every error response should include a request ID that correlates to server logs. This enables debugging without requiring users to share sensitive information.

# Middleware to add request ID to all responses
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
    request.state.request_id = request_id

    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

Sanitize Error Messages

Never expose internal implementation details in error messages to clients. Instead, log detailed information server-side and return generic messages to clients.

# Bad - exposing internal details
raise APIException(
    code=ErrorCode.INTERNAL_ERROR,
    message="Database connection failed: postgresql://user:pass@host:5432"
)

# Good - generic message with logged details
logger.error(f"Database connection failed: {db_error}", exc_info=True)
raise APIException(
    code=ErrorCode.INTERNAL_ERROR,
    message="A service error occurred. Please try again later."
)

Provide Actionable Messages

Error messages should tell users what they can do to resolve the issue.

# Bad
{"message": "Invalid input"}

# Good
{"message": "Email address is invalid", "details": {"field": "email", "hint": "Use format: user@example.com"}}

Use Rate Limiting Headers

When returning 429 responses, include headers that inform clients about rate limits.

response = JSONResponse(
    status_code=429,
    content={"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests"}},
    headers={
        "Retry-After": "60",
        "X-RateLimit-Limit": "100",
        "X-RateLimit-Remaining": "0",
        "X-RateLimit-Reset": "1708000000"
    }
)

Testing Error Handling

Write tests that verify your error responses match the expected format:

import pytest

def test_resource_not_found_returns_proper_format(client):
    response = client.get("/users/nonexistent-id")

    assert response.status_code == 404
    data = response.json()

    assert "error" in data
    assert data["error"]["code"] == "RESOURCE_NOT_FOUND"
    assert "message" in data["error"]
    assert "timestamp" in data["error"]
    assert "request_id" in data["error"]

def test_validation_error_includes_field_details(client):
    response = client.post("/users", json={"email": "invalid"})

    assert response.status_code == 422
    data = response.json()

    assert data["error"]["code"] == "VALIDATION_ERROR"
    assert "email" in data["error"]["details"]["field_errors"]

Generating Error Handling Code with Claude Code

Claude Code generates consistent error handling scaffolding across languages when prompted with your API’s error taxonomy. Provide the error codes you’ve defined and ask for the full exception hierarchy:

# Claude Code prompt
claude "Generate a complete error handling module for a FastAPI service.
Error codes are defined in this enum: AUTH_INVALID_TOKEN, AUTH_EXPIRED_TOKEN,
VALIDATION_MISSING_FIELD, VALIDATION_INVALID_FORMAT, RESOURCE_NOT_FOUND,
RESOURCE_CONFLICT, RATE_LIMIT_EXCEEDED, INTERNAL_ERROR.

Requirements:
- Custom exception class hierarchy with HTTP status code mapping
- Global exception handlers for FastAPI
- Request ID middleware that propagates through all error responses
- Log structured JSON for server errors (5xx), skip logging for client errors (4xx)
- Never expose stack traces or internal paths in 4xx responses"

Claude Code generates the full module including the exception hierarchy, HTTP status mapping, and structured logging differentiated by error category. The key strength is the differentiated logging rule — logging every 4xx clutters observability dashboards with client mistakes, while silently dropping 5xx errors hides real bugs.

Consistent Error Handling Across Microservices

In microservice architectures, inconsistent error formats between services create client-side parsing complexity. Define a shared error contract and use Claude Code to generate language-specific implementations that conform to it.

Shared contract (published as a JSON Schema or OpenAPI component):

# Shared error schema — reference this in all service OpenAPI specs
components:
  schemas:
    APIError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, timestamp, request_id]
          properties:
            code:
              type: string
              pattern: '^[A-Z][A-Z0-9_]*$'
            message:
              type: string
              maxLength: 500
            details:
              type: object
              additionalProperties: true
            timestamp:
              type: string
              format: date-time
            request_id:
              type: string

Prompt Claude Code with this schema and your service’s language to generate conforming implementations across Node.js, Python, and Go services. Each implementation handles the protocol differently (Express middleware, FastAPI exception handlers, Go middleware functions) but produces identical JSON output.

Handling Upstream Errors and Error Translation

API services that call other services must translate upstream errors rather than forwarding them directly. Forwarding a raw database error or an internal service’s 500 response leaks implementation details and breaks the contract with your clients.

Claude Code handles this translation pattern well:

async def fetch_user_from_upstream(user_id: str) -> dict:
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{USER_SERVICE_URL}/users/{user_id}",
                timeout=5.0
            )

        if response.status_code == 404:
            # Translate upstream 404 to our standard error
            raise APIException(
                code=ErrorCode.RESOURCE_NOT_FOUND,
                message=f"User {user_id} not found",
                status_code=404
            )
        if response.status_code >= 500:
            # Upstream 5xx becomes our 502 (bad gateway)
            logger.error(f"User service error: {response.status_code} {response.text}")
            raise APIException(
                code=ErrorCode.SERVICE_UNAVAILABLE,
                message="User service is temporarily unavailable. Please retry.",
                status_code=502
            )

        response.raise_for_status()
        return response.json()

    except httpx.TimeoutException:
        logger.error(f"User service timeout for user_id={user_id}")
        raise APIException(
            code=ErrorCode.SERVICE_UNAVAILABLE,
            message="Request timed out. Please retry in a moment.",
            status_code=504
        )
    except httpx.ConnectError:
        logger.error(f"Cannot connect to user service")
        raise APIException(
            code=ErrorCode.SERVICE_UNAVAILABLE,
            message="Service temporarily unavailable.",
            status_code=503
        )

The pattern — catch upstream errors, log the raw details server-side, raise a translated exception with a client-appropriate message — is the standard that Claude Code applies when you specify “translate upstream errors rather than propagating them.”

Built by theluckystrike — More at zovo.one