Map domain-level business failures into a standardized, structured API error envelope with zero boilerplate in your route handlers.
The Challenge
Standard FastAPI error handling often leads to inconsistent API responses:
- Scattered Error Logic: Each endpoint or service layer manually catches failures and returns ad-hoc dictionaries like
{"detail": "Not found"}or{"error": "Invalid data"}, creating a fragmented API contract for frontend consumers. - Missing Diagnostic Context: Internal details (exception type, metadata payload) are lost when converting to HTTP responses, making debugging production incidents harder.
- Inconsistent HTTP Codes: The same logical error (e.g., "entity not found") might return 404 in one endpoint, 500 in another, and 400 in a third, depending on where the exception was raised.
The ZCore Elegance
ZCore provides a thin AppException hierarchy that maps directly to HTTP status codes, plus a single global handler that catches them all and formats them into the standard ResponseWrapper envelope. Services and repositories raise typed exceptions without any HTTP awareness.
from zcore.exceptions import EntityNotFound, ValidationError, ForbiddenError
class PaymentService(BaseService):
async def process(self, payment_id: uuid.UUID):
payment = await self.repository.get(payment_id)
if not payment:
raise EntityNotFound("Payment record not found")
if not payment.is_approved:
raise ForbiddenError("Insufficient permissions to process")
if payment.amount <= 0:
raise ValidationError("Payment amount must be positive")
return payment
# FastAPI automatically returns:
# 404 → {"success": false, "message": "Payment record not found", "meta": {"error_type": "EntityNotFound", "payload": null}}
from fastapi import HTTPException
class PaymentService:
async def process(self, payment_id: uuid.UUID):
payment = await self.repository.get(payment_id)
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
if not payment.is_approved:
raise HTTPException(status_code=403, detail="Forbidden")
if payment.amount <= 0:
raise HTTPException(status_code=400, detail="Invalid amount")
return payment
# No standard envelope across endpoints
# No automatic error type classification in response
# Manual HTTP code management scattered through business logic
Boundaries & Integration
The exception handler is a lightweight, opt-in component registered at the application level.
- FastAPI Standard: The
app_exception_handleris a standard FastAPI exception handler registered viaapp.add_exception_handler(AppException, app_exception_handler). It works with any FastAPI application, including those not using other ZCore components. - Payload Customization: Each exception can carry an optional
payloaddictionary. The handler includes this payload in the responsemetaobject, enabling structured error details (e.g., field-level validation errors or conflict resource identifiers). - Third-Party Compatibility: Since the handler only catches
AppExceptionsubclasses, standard Python exceptions (e.g.,ValueError,KeyError) and FastAPI's built-inHTTPExceptionare unaffected and handled by their default mechanisms.
Under-the-Hood Spec
1. Status Code Mapping via Class Attribute
Each AppException subclass defines status_code as a class-level integer [exceptions/base.py]. EntityNotFound maps to 404, ValidationError to 400, AuthError to 401, ForbiddenError to 403, and DuplicateEntity to 409. Custom exceptions inherit from AppException and can override status_code to any valid HTTP status.
2. Structured Logging Capture
The app_exception_handler logs every exception through structlog before constructing the response [exceptions/handlers.py]. The log entry includes type, status_code, message, payload, path, and method—providing a complete audit trail for debugging without exposing sensitive internals to the client.
3. Standard API Error Envelope
The handler constructs a ResponseWrapper[None] with success=False [exceptions/handlers.py]. The message field carries the human-readable error string, while meta.error_type stores the exact exception class name (e.g., "EntityNotFound") for programmatic consumption. The meta.payload field is included only if the exception was raised with contextual metadata.
Registration in main.py
The exception handler must be explicitly registered during application bootstrap:
python
from zcore.exceptions import AppException, app_exception_handler
app.add_exception_handler(AppException, app_exception_handler)
Extending the Hierarchy
To add a custom error type, subclass AppException and set status_code:
python
class RateLimitExceeded(AppException):
status_code = 429
The existing handler automatically picks up the new type at runtime.