How to handle and raise domain exceptions
Raise built-in domain exceptions, create custom business errors, and return standardized JSON error envelopes.
ZCore eliminates inconsistent error payloads across endpoints. Raising any AppException subclass or encountering runtime validation errors automatically formats the response into a unified ResponseWrapper envelope with the correct HTTP status code.
1. Raising Built-In Exceptions in Services
Raise typed exceptions directly inside your services, repositories, or hooks:
# tasks/services.py
import uuid
from zcore import BaseService
from zcore.exceptions import EntityNotFound, ValidationError
from .models import Task
from .repositories import TaskRepo
from .schemas import TaskCreate
class TaskService(BaseService[Task]):
def __init__(self, repo: TaskRepo):
super().__init__(model=Task, repository=repo)
async def mark_task_as_done(self, task_id: uuid.UUID):
task = await self.repository.get(id=task_id)
if not task:
# Raises 404 with structured payload context
raise EntityNotFound(
message=f"Task with ID {task_id} does not exist.",
payload={"task_id": str(task_id)}
)
if task.is_completed:
# Raises 400 Bad Request
raise ValidationError(
message="Task is already marked as completed."
)
return await self.repository.update(task_id, {"is_completed": True})2. Creating Custom Business Exceptions
To define domain-specific exceptions, subclass AppException and specify your desired status_code:
# tasks/exceptions.py
from zcore.exceptions import AppException
class TaskLimitExceeded(AppException):
status_code = 422 # Unprocessable Entity
def __init__(self, current_count: int, max_limit: int):
super().__init__(
message=f"Account task limit reached ({current_count}/{max_limit}).",
payload={"current_count": current_count, "max_limit": max_limit}
)
class PaymentRequired(AppException):
status_code = 402 # Payment Required3. Registering Unified Handlers in main.py
ZCore provides a single registration function, register_exception_handlers, that registers normalization handlers for all exception categories:
# main.py
from fastapi import FastAPI
from zcore import register_exception_handlers
app = FastAPI(...)
# Normalizes domain errors, Pydantic 422s, HTTPExceptions, and 500 runtime errors
register_exception_handlers(app)You can also selectively toggle specific handlers using boolean flags:
register_exception_handlers(
app,
include_app_exceptions=True,
include_validation_exceptions=True,
include_http_exceptions=True,
include_response_validation_exceptions=True,
include_unhandled_exceptions=True,
)Uniform Response Envelope:
Every error intercepted by ZCore is serialized into a standard ResponseWrapper[None] structure:
{
"success": false,
"message": "Account task limit reached (100/100).",
"data": null,
"meta": {
"error_type": "TaskLimitExceeded",
"payload": {
"current_count": 100,
"max_limit": 100
}
}
}Handlers & Debug-Gated Security
register_exception_handlers(app) attaches five dedicated exception handlers:
| Exception Category | HTTP Status | Response Handling |
|---|---|---|
AppException | exc.status_code | Extracts custom message and context payload into meta.payload. |
RequestValidationError | 422 | Converts Pydantic error tuples into clean dot-separated paths (e.g. body.items.0.title) and returns sanitized error summaries. |
StarletteHTTPException | exc.status_code | Preserves custom HTTP headers and maps the detail message. |
ResponseValidationError | 500 | Catches internal endpoint output schema mismatches. |
Exception (Catch-all) | 500 | Catches any uncaught runtime errors to prevent server crashes. |
Production Data Leak Protection (Debug-Gating):
For ResponseValidationError and generic Exception (500 errors), full stack traces and diagnostic details are only rendered in the JSON response when settings.DEBUG = True.
In production (DEBUG = False), internal error details are masked with a safe, generic message ("Internal server error") while the complete stack trace is safely recorded in your structured logs.
How to listen to and dispatch domain events
Subscribe to event channels using @on_event decorators, register listeners in plugins, and dispatch events safely.
How to mask sensitive fields dynamically
Use Zchema to automatically prune fields from API responses, sanitize inputs, and mask OpenAPI schemas.