ZCore LogoZCore
How to

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 Required

3. 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 CategoryHTTP StatusResponse Handling
AppExceptionexc.status_codeExtracts custom message and context payload into meta.payload.
RequestValidationError422Converts Pydantic error tuples into clean dot-separated paths (e.g. body.items.0.title) and returns sanitized error summaries.
StarletteHTTPExceptionexc.status_codePreserves custom HTTP headers and maps the detail message.
ResponseValidationError500Catches internal endpoint output schema mismatches.
Exception (Catch-all)500Catches 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.

On this page