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)
-
200 OK - Request succeeded for GET, PUT, or DELETE
-
201 Created - New resource successfully created
-
204 No Content - Successful deletion or empty response for PUT
Client Error Codes (4xx)
-
400 Bad Request - Invalid request syntax or parameters
-
401 Unauthorized - Missing or invalid authentication
-
403 Forbidden - Authenticated but not authorized
-
404 Not Found - Resource does not exist
-
409 Conflict - Resource state conflicts (duplicate, versioning issue)
-
422 Unprocessable Entity - Valid syntax but semantic errors
-
429 Too Many Requests - Rate limit exceeded
Server Error Codes (5xx)
-
500 Internal Server Error - Unexpected server failure
-
502 Bad Gateway - Upstream service unavailable
-
503 Service Unavailable - Temporary overload or maintenance
-
504 Gateway Timeout - Upstream timeout
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.”
Related Articles
- Writing Claude Md Files That Teach AI Your Project Specific
- Best AI Tools for Writing Idiomatic Rust Error Handling
- Copilot vs Cursor for Writing Rust Error Handling with
- How to Optimize AI Coding Prompts for Generating Production
- How to Use the Claude API for Automated Code Review
Built by theluckystrike — More at zovo.one