PyFly provides a comprehensive, structured approach to error handling. Every framework exception carries machine-readable metadata, maps automatically to the correct HTTP status code, and produces RFC 7807-inspired error responses. This guide covers the full exception hierarchy, the error response model, and strategies for using them effectively in your services.
- Introduction
- Exception Hierarchy
- PyFlyException
- Business Exceptions
- Security Exceptions
- Infrastructure Exceptions
- External Service Exceptions
- HTTP Status Mapping
- ErrorResponse
- ErrorCategory Enum
- ErrorSeverity Enum
- FieldError Dataclass
- Complete Example
PyFly's error handling philosophy is built on four principles:
-
Structured exceptions. Every exception carries a
message,code, andcontextdict -- not just a string. This enables machine-readable error handling at every layer. -
Categorical hierarchy. Exceptions are organized by domain: business logic, security, infrastructure, and external services. Catch an entire category or a specific exception.
-
Automatic HTTP mapping. The web layer's global exception handler maps every
PyFlyExceptionsubclass to the appropriate HTTP status code. You never need to manually set status codes for standard error cases. -
RFC 7807-inspired responses. The
ErrorResponsedataclass provides a comprehensive error payload with tracing, classification, and field-level details.
from pyfly.kernel import (
# Base
PyFlyException,
# Business
BusinessException, ValidationException, ResourceNotFoundException,
ConflictException, InvalidRequestException,
# Security
SecurityException, UnauthorizedException, ForbiddenException,
# Infrastructure
InfrastructureException, ServiceUnavailableException,
CircuitBreakerException, RateLimitException,
# Types
ErrorResponse, ErrorCategory, ErrorSeverity, FieldError,
)PyFlyException
|
+-- BusinessException
| +-- ValidationException
| +-- ResourceNotFoundException
| +-- ConflictException
| +-- PreconditionFailedException
| +-- GoneException
| +-- InvalidRequestException
| +-- DataIntegrityException
| +-- ConcurrencyException
| +-- LockedResourceException
| +-- MethodNotAllowedException
| +-- UnsupportedMediaTypeException
| +-- PayloadTooLargeException
|
+-- SecurityException
| +-- UnauthorizedException
| +-- ForbiddenException
| +-- AuthorizationException
|
+-- InfrastructureException
| +-- ServiceUnavailableException
| +-- CircuitBreakerException
| +-- RateLimitException
| | +-- QuotaExceededException
| +-- BulkheadException
| +-- OperationTimeoutException
| +-- RetryExhaustedException
| +-- DegradedServiceException
| +-- NotImplementedException
| |
| +-- ExternalServiceException
| +-- ThirdPartyServiceException
| +-- BadGatewayException
| +-- GatewayTimeoutException
This structure lets you catch at any level of granularity:
try:
await order_service.create_order(data)
except ValidationException:
# Handle validation specifically
except BusinessException:
# Handle any business rule violation
except PyFlyException:
# Handle any framework exceptionSource: src/pyfly/kernel/exceptions.py
The root of the exception hierarchy. All PyFly exceptions inherit from this class, enabling unified error handling.
class PyFlyException(Exception):
def __init__(
self,
message: str,
code: str | None = None,
context: dict | None = None,
) -> None:Constructor Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
str |
required | Human-readable error description |
code |
str | None |
None |
Machine-readable error code (e.g. "ORDER_001") |
context |
dict | None |
None |
Arbitrary key-value pairs for debugging context |
Usage:
raise PyFlyException(
"Something went wrong",
code="INTERNAL_ERROR",
context={"operation": "create_order", "customer_id": "cust-42"},
)Accessing fields:
try:
...
except PyFlyException as exc:
print(str(exc)) # "Something went wrong"
print(exc.code) # "INTERNAL_ERROR"
print(exc.context) # {"operation": "create_order", "customer_id": "cust-42"}Business exceptions represent domain rule violations and client errors. They generally map to 4xx HTTP status codes.
Base class for all domain logic errors.
raise BusinessException("Order total exceeds credit limit", code="CREDIT_LIMIT")HTTP Status: 400 (catch-all for unspecified business exceptions)
Raised when input data fails validation rules.
raise ValidationException(
"Invalid order data",
code="VALIDATION_ERROR",
context={
"errors": [
{"loc": ["quantity"], "msg": "must be positive"},
{"loc": ["email"], "msg": "invalid format"},
]
},
)HTTP Status: 422 Unprocessable Entity
When to use: Invalid input fields, Pydantic validation failures, custom business rule validation.
Raised when a requested entity does not exist.
raise ResourceNotFoundException(
"Order not found",
code="ORDER_NOT_FOUND",
context={"order_id": "ord-999"},
)HTTP Status: 404 Not Found
When to use: Database lookup returns no result, entity has not been created.
Raised when an operation conflicts with the current state.
raise ConflictException(
"Order already exists",
code="ORDER_DUPLICATE",
context={"order_id": "ord-001"},
)HTTP Status: 409 Conflict
When to use: Duplicate creation, version mismatch on update, state machine violation.
Raised when a precondition for the operation was not met.
raise PreconditionFailedException(
"Order must be in PENDING state to be confirmed",
code="INVALID_STATE_TRANSITION",
context={"current_state": "shipped"},
)HTTP Status: 412 Precondition Failed
When to use: Conditional updates (If-Match header), state preconditions.
Raised when a resource has been permanently removed.
raise GoneException(
"This order was permanently deleted",
code="ORDER_DELETED",
context={"order_id": "ord-001", "deleted_at": "2026-01-01T00:00:00Z"},
)HTTP Status: 410 Gone
When to use: Soft-deleted resources, expired links, decommissioned endpoints.
Raised when a request is syntactically valid but semantically incorrect.
raise InvalidRequestException(
"Cannot ship to a PO Box with express delivery",
code="INVALID_SHIPPING_COMBO",
)HTTP Status: 400 Bad Request
When to use: Business logic rejects the request despite it being well-formed.
Raised when a data integrity constraint is violated.
raise DataIntegrityException(
"Foreign key constraint: customer does not exist",
code="FK_VIOLATION",
context={"customer_id": "cust-nonexistent"},
)When to use: Database constraint violations, referential integrity errors.
Raised on concurrent modification conflicts.
raise ConcurrencyException(
"Order was modified by another process",
code="OPTIMISTIC_LOCK_FAILURE",
context={"expected_version": 3, "actual_version": 5},
)When to use: Optimistic locking failures, compare-and-swap mismatches.
Raised when a resource is locked and cannot be modified.
raise LockedResourceException(
"Order is locked for processing",
code="ORDER_LOCKED",
context={"order_id": "ord-001", "locked_by": "batch-job-42"},
)HTTP Status: 423 Locked
When to use: Pessimistic locking, administrative locks.
Raised when the requested operation is not allowed on the resource.
raise MethodNotAllowedException(
"Cannot cancel a shipped order",
code="CANCEL_NOT_ALLOWED",
)HTTP Status: 405 Method Not Allowed
Raised when the content type is not supported.
raise UnsupportedMediaTypeException(
"Only JSON content is accepted",
code="UNSUPPORTED_CONTENT_TYPE",
)HTTP Status: 415 Unsupported Media Type
Raised when the request payload exceeds the maximum allowed size.
raise PayloadTooLargeException(
"File upload exceeds 10MB limit",
code="FILE_TOO_LARGE",
context={"max_size_mb": 10, "actual_size_mb": 25},
)HTTP Status: 413 Payload Too Large
Security exceptions represent authentication and authorization failures. They map to 401 and 403 HTTP status codes.
Base class for all security-related errors.
raise SecurityException("Security violation", code="SECURITY_ERROR")HTTP Status: 401 (catch-all)
Raised when authentication is required but not provided or invalid.
raise UnauthorizedException(
"Invalid or expired token",
code="TOKEN_EXPIRED",
context={"token_type": "Bearer"},
)HTTP Status: 401 Unauthorized
When to use: Missing credentials, expired token, invalid signature.
Raised when the authenticated caller lacks permission.
raise ForbiddenException(
"Insufficient permissions to delete orders",
code="ACCESS_DENIED",
context={"required_role": "admin", "user_role": "viewer"},
)HTTP Status: 403 Forbidden
When to use: User is authenticated but not authorized for this action.
Raised when an authorization policy denies access.
raise AuthorizationException(
"Policy 'org-member-only' denied access",
code="POLICY_DENIED",
context={"policy": "org-member-only"},
)When to use: Fine-grained policy-based access control decisions.
Infrastructure exceptions represent system-level failures: database outages, circuit breaker trips, rate limiting, and timeouts.
Base class for all infrastructure errors.
raise InfrastructureException("Database connection pool exhausted")HTTP Status: 502 (catch-all)
Raised when a downstream service is unavailable.
raise ServiceUnavailableException(
"Database is unavailable",
code="DB_UNAVAILABLE",
)HTTP Status: 503 Service Unavailable
Raised when a circuit breaker is open.
raise CircuitBreakerException(
"Circuit breaker open for payment-service",
code="CIRCUIT_OPEN",
context={"service": "payment-service", "failures": 5},
)HTTP Status: 503 Service Unavailable
Raised when a rate limit is exceeded.
raise RateLimitException(
"Rate limit exceeded: 100 requests per minute",
code="RATE_LIMIT",
context={"limit": 100, "window": "1m"},
)HTTP Status: 429 Too Many Requests
Raised when an API or resource quota is exhausted. Inherits from RateLimitException.
raise QuotaExceededException(
"Monthly API quota exceeded",
code="QUOTA_EXCEEDED",
context={"quota": 10000, "used": 10001},
)HTTP Status: 429 Too Many Requests
Raised when bulkhead capacity is exhausted.
raise BulkheadException(
"Bulkhead capacity exhausted for inventory-service",
code="BULKHEAD_FULL",
)HTTP Status: 503 Service Unavailable
When to use: Concurrent request limit reached, protecting the system from cascade failures.
Raised when an operation exceeds its time limit.
raise OperationTimeoutException(
"Database query timed out after 30s",
code="QUERY_TIMEOUT",
context={"timeout_seconds": 30},
)HTTP Status: 504 Gateway Timeout
Raised when all retry attempts have been exhausted.
raise RetryExhaustedException(
"Failed after 3 retries to reach inventory-service",
code="RETRY_EXHAUSTED",
context={"max_retries": 3, "last_error": "Connection refused"},
)When to use: Retry policies have been fully exhausted.
Raised when a service is running in a degraded state.
raise DegradedServiceException(
"Running without cache -- degraded performance expected",
code="DEGRADED_MODE",
)HTTP Status: 503 Service Unavailable
Raised when a requested operation is not yet implemented.
raise NotImplementedException(
"Bulk order import is not yet available",
code="NOT_IMPLEMENTED",
)HTTP Status: 501 Not Implemented
External service exceptions represent failures in communication with third-party or
upstream services. They inherit from InfrastructureException.
Base class for external/third-party service failures.
raise ExternalServiceException(
"Failed to call shipping API",
code="SHIPPING_API_ERROR",
)Raised when a third-party service returns an error.
raise ThirdPartyServiceException(
"Stripe returned error: card_declined",
code="STRIPE_ERROR",
context={"stripe_code": "card_declined"},
)Raised when an upstream service returns an invalid response.
raise BadGatewayException(
"Inventory service returned malformed response",
code="BAD_GATEWAY",
)HTTP Status: 502 Bad Gateway
Raised when an upstream service does not respond in time.
raise GatewayTimeoutException(
"Inventory service did not respond within 10s",
code="GATEWAY_TIMEOUT",
context={"upstream": "inventory-service", "timeout": 10},
)HTTP Status: 504 Gateway Timeout
The global exception handler in src/pyfly/web/adapters/starlette/errors.py maps
exceptions to HTTP status codes. The mapping uses most-specific-first ordering:
| Exception | HTTP Status | HTTP Status Name |
|---|---|---|
ValidationException |
422 | Unprocessable Entity |
ResourceNotFoundException |
404 | Not Found |
ConflictException |
409 | Conflict |
PreconditionFailedException |
412 | Precondition Failed |
GoneException |
410 | Gone |
InvalidRequestException |
400 | Bad Request |
LockedResourceException |
423 | Locked |
MethodNotAllowedException |
405 | Method Not Allowed |
UnsupportedMediaTypeException |
415 | Unsupported Media Type |
PayloadTooLargeException |
413 | Payload Too Large |
UnauthorizedException |
401 | Unauthorized |
ForbiddenException |
403 | Forbidden |
SecurityException |
401 | Unauthorized |
QuotaExceededException |
429 | Too Many Requests |
RateLimitException |
429 | Too Many Requests |
CircuitBreakerException |
503 | Service Unavailable |
BulkheadException |
503 | Service Unavailable |
ServiceUnavailableException |
503 | Service Unavailable |
DegradedServiceException |
503 | Service Unavailable |
OperationTimeoutException |
504 | Gateway Timeout |
NotImplementedException |
501 | Not Implemented |
BadGatewayException |
502 | Bad Gateway |
GatewayTimeoutException |
504 | Gateway Timeout |
BusinessException (catch-all) |
400 | Bad Request |
InfrastructureException (catch-all) |
502 | Bad Gateway |
Non-PyFlyException |
500 | Internal Server Error |
For non-PyFlyException errors, the handler returns a generic 500 response
without leaking internal details.
ErrorResponse is an RFC 7807-inspired dataclass for building structured error
payloads. It provides comprehensive metadata for error tracking, classification,
and client-side handling.
from pyfly.kernel import ErrorResponse, ErrorCategory, ErrorSeverity, FieldErrorThese fields are always present in the serialized output:
| Field | Type | Description |
|---|---|---|
timestamp |
str |
ISO 8601 timestamp of the error |
status |
int |
HTTP status code |
error |
str |
HTTP status text (e.g. "Not Found") |
message |
str |
Human-readable error description |
code |
str |
Machine-readable error code |
path |
str |
Request path that triggered the error |
category |
ErrorCategory |
Error classification |
severity |
ErrorSeverity |
Error severity level |
retryable |
bool |
Whether the client should retry |
These fields are included in to_dict() output only when non-None or non-empty:
| Field | Type | Default | Description |
|---|---|---|---|
trace_id |
str | None |
None |
Distributed trace ID |
span_id |
str | None |
None |
Span ID |
transaction_id |
str | None |
None |
Transaction ID |
retry_after |
int | None |
None |
Seconds to wait before retry |
field_errors |
list[FieldError] |
[] |
Validation field errors |
debug_info |
dict | None |
None |
Debug information |
suggestion |
str | None |
None |
Suggested corrective action |
documentation_url |
str | None |
None |
Link to relevant documentation |
Serializes the ErrorResponse to a dictionary suitable for JSON responses:
response = ErrorResponse(
timestamp="2026-01-15T10:30:00Z",
status=422,
error="Unprocessable Entity",
message="Validation failed",
code="VALIDATION_ERROR",
path="/api/orders",
category=ErrorCategory.VALIDATION,
severity=ErrorSeverity.LOW,
retryable=False,
field_errors=[
FieldError(field="quantity", message="must be positive", rejected_value=-1),
FieldError(field="email", message="invalid format", rejected_value="not-an-email"),
],
suggestion="Check the field_errors array for details on each invalid field.",
)
json_dict = response.to_dict()
# {
# "timestamp": "2026-01-15T10:30:00Z",
# "status": 422,
# "error": "Unprocessable Entity",
# "message": "Validation failed",
# "code": "VALIDATION_ERROR",
# "path": "/api/orders",
# "category": "VALIDATION",
# "severity": "LOW",
# "retryable": False,
# "field_errors": [
# {"field": "quantity", "message": "must be positive", "rejected_value": -1},
# {"field": "email", "message": "invalid format", "rejected_value": "not-an-email"}
# ],
# "suggestion": "Check the field_errors array for details on each invalid field."
# }Source: src/pyfly/kernel/types.py
Classifies errors by their origin or domain:
from pyfly.kernel import ErrorCategory
class ErrorCategory(Enum):
VALIDATION = "VALIDATION" # Input validation failures
BUSINESS = "BUSINESS" # Business rule violations
TECHNICAL = "TECHNICAL" # Internal technical errors
SECURITY = "SECURITY" # Authentication/authorization failures
EXTERNAL = "EXTERNAL" # Third-party service failures
RESOURCE = "RESOURCE" # Resource access issues (not found, gone)
RATE_LIMIT = "RATE_LIMIT" # Rate limiting / quota exceeded
CIRCUIT_BREAKER = "CIRCUIT_BREAKER" # Circuit breaker open| Category | When to Use |
|---|---|
VALIDATION |
Input does not conform to the expected schema |
BUSINESS |
Domain rules prevent the operation |
TECHNICAL |
Internal errors (bugs, configuration issues) |
SECURITY |
Authentication or authorization failed |
EXTERNAL |
A third-party dependency failed |
RESOURCE |
The target resource is missing or inaccessible |
RATE_LIMIT |
Request rate or quota limit exceeded |
CIRCUIT_BREAKER |
Circuit breaker is preventing requests |
Indicates how severe an error is, useful for alerting and monitoring:
from pyfly.kernel import ErrorSeverity
class ErrorSeverity(Enum):
LOW = "LOW" # Minor issues, informational
MEDIUM = "MEDIUM" # Standard errors, default severity
HIGH = "HIGH" # Significant errors requiring attention
CRITICAL = "CRITICAL" # System-threatening errors, page immediately| Severity | Typical Use |
|---|---|
LOW |
Validation errors, expected failures |
MEDIUM |
Business rule violations, not-found errors |
HIGH |
External service failures, security violations |
CRITICAL |
Database outages, data corruption |
FieldError describes a validation error on a single field. Used within
ErrorResponse.field_errors to provide detailed, per-field error information.
from pyfly.kernel import FieldError
@dataclass(frozen=True)
class FieldError:
field: str
message: str
rejected_value: Any = None| Field | Type | Description |
|---|---|---|
field |
str |
The field name that failed validation |
message |
str |
Human-readable error message for the field |
rejected_value |
Any |
The value that was rejected (optional) |
Usage:
FieldError(field="quantity", message="must be positive", rejected_value=-1)
FieldError(field="email", message="invalid email format", rejected_value="not-email")
FieldError(field="name", message="required") # rejected_value defaults to NoneThe following example shows a comprehensive error handling strategy for an order microservice.
"""order_service/services.py"""
from datetime import UTC, datetime
from pyfly.container import service
from pyfly.kernel import (
ResourceNotFoundException,
ValidationException,
ConflictException,
ConcurrencyException,
ForbiddenException,
ServiceUnavailableException,
ErrorResponse,
ErrorCategory,
ErrorSeverity,
FieldError,
)
@service
class OrderService:
"""Order service demonstrating comprehensive error handling."""
def __init__(self, order_repo, inventory_client, auth_service) -> None:
self._repo = order_repo
self._inventory = inventory_client
self._auth = auth_service
async def create_order(self, user_id: str, data: dict) -> dict:
"""Create an order with multi-layer error handling."""
# 1. Authorization check
if not await self._auth.can_create_orders(user_id):
raise ForbiddenException(
"User is not authorized to create orders",
code="ORDER_CREATE_FORBIDDEN",
context={"user_id": user_id},
)
# 2. Business validation
if not data.get("items"):
raise ValidationException(
"Order must contain at least one item",
code="EMPTY_ORDER",
context={
"errors": [
{"loc": ["items"], "msg": "at least one item required"}
]
},
)
# 3. Duplicate check
existing = await self._repo.find_by_idempotency_key(
data.get("idempotency_key")
)
if existing:
raise ConflictException(
"Order with this idempotency key already exists",
code="DUPLICATE_ORDER",
context={"existing_order_id": existing["id"]},
)
# 4. Inventory check (external service)
try:
await self._inventory.reserve_items(data["items"])
except Exception as e:
raise ServiceUnavailableException(
"Inventory service is unavailable",
code="INVENTORY_UNAVAILABLE",
context={"original_error": str(e)},
)
# 5. Create the order
order = await self._repo.save({
"customer_id": user_id,
"items": data["items"],
"status": "created",
})
return order
async def update_order(self, order_id: str, version: int, data: dict) -> dict:
"""Update with optimistic locking."""
order = await self._repo.find_by_id(order_id)
if order is None:
raise ResourceNotFoundException(
f"Order {order_id} not found",
code="ORDER_NOT_FOUND",
context={"order_id": order_id},
)
if order["version"] != version:
raise ConcurrencyException(
"Order was modified by another process",
code="VERSION_MISMATCH",
context={
"expected_version": version,
"actual_version": order["version"],
},
)
order.update(data)
return await self._repo.save(order)
# =========================================================================
# Building ErrorResponse manually (for custom error endpoints)
# =========================================================================
def build_validation_error_response(
path: str,
field_errors: list[FieldError],
) -> ErrorResponse:
"""Build a structured validation error response."""
return ErrorResponse(
timestamp=datetime.now(UTC).isoformat(),
status=422,
error="Unprocessable Entity",
message="One or more fields failed validation",
code="VALIDATION_ERROR",
path=path,
category=ErrorCategory.VALIDATION,
severity=ErrorSeverity.LOW,
retryable=False,
field_errors=field_errors,
suggestion="Review the field_errors array and correct the invalid fields.",
)
def build_rate_limit_error_response(
path: str,
retry_after: int,
) -> ErrorResponse:
"""Build a rate limit error response with retry guidance."""
return ErrorResponse(
timestamp=datetime.now(UTC).isoformat(),
status=429,
error="Too Many Requests",
message="Rate limit exceeded",
code="RATE_LIMIT",
path=path,
category=ErrorCategory.RATE_LIMIT,
severity=ErrorSeverity.MEDIUM,
retryable=True,
retry_after=retry_after,
suggestion=f"Retry after {retry_after} seconds.",
)
# =========================================================================
# Usage example
# =========================================================================
# Build a validation error response
response = build_validation_error_response(
path="/api/orders",
field_errors=[
FieldError("quantity", "must be positive", rejected_value=-1),
FieldError("email", "invalid format", rejected_value="bad-email"),
],
)
# Serialize to JSON-friendly dict
json_body = response.to_dict()
# Returns a dict with all core fields plus field_errors and suggestionThe global exception handler automatically produces similar responses for any
PyFlyException thrown during request processing. The service code above only needs
to raise the appropriate exception -- the web layer handles serialization and HTTP
status code mapping automatically.