Skip to content

Exceptions

The exception hierarchy lives in website/exceptions/ and provides structured, domain-specific errors used throughout the service layer.

Hierarchy

QuestMasterError             # Base for all application errors
  +-- NotFoundError          # Resource not found
  +-- UnauthorizedError      # Permission denied
  +-- ValidationError        # Input validation failure
  +-- DatabaseError          # Database operation failure
  +-- DiscordError           # Discord integration base error
  |     +-- DiscordAPIError  # Discord API call failure
  +-- GameError              # Game-related base error
        +-- GameFullError          # Game has no open slots
        +-- GameClosedError        # Game is not accepting registrations
        +-- DuplicateRegistrationError  # User already registered
        +-- SessionConflictError        # Session time conflict

Usage

Services raise these exceptions. Views catch them and translate to appropriate HTTP responses.

from website.exceptions import GameFullError

def register_player(game_id: int, user: User) -> None:
    game = game_repo.get_by_id(game_id)
    if game.is_full:
        raise GameFullError(f"Game '{game.name}' is full")
    # ...

API Reference

Custom exception hierarchy for the QuestMaster application.

NotFoundError

Bases: QuestMasterError

Resource not found.

Source code in website/exceptions/base.py
class NotFoundError(QuestMasterError):
    """Resource not found."""

    http_status = 404

    def __init__(self, message: str, resource_type=None, resource_id=None, **kwargs):
        kwargs.setdefault("code", "NOT_FOUND")
        details = kwargs.pop("details", {})
        if resource_type is not None:
            details["resource_type"] = resource_type
        if resource_id is not None:
            details["resource_id"] = resource_id
        super().__init__(message, details=details, **kwargs)

QuestMasterError

Bases: Exception

Base exception for all QuestMaster errors.

Parameters:

Name Type Description Default
message str

Human-readable error description.

required
code str

Machine-readable error code (e.g. "GAME_FULL").

None
details dict

Additional context about the error.

None
Source code in website/exceptions/base.py
class QuestMasterError(Exception):
    """Base exception for all QuestMaster errors.

    Args:
        message: Human-readable error description.
        code: Machine-readable error code (e.g. "GAME_FULL").
        details: Additional context about the error.
    """

    http_status = None

    def __init__(self, message: str, code: str = None, details: dict = None):
        self.message = message
        self.code = code
        self.details = details or {}
        super().__init__(self.message)

    def to_dict(self):
        """Serialize the error for API responses."""
        result = {"error": self.message, "code": self.code}
        if self.details:
            result["details"] = self.details
        return result

    def __repr__(self):
        parts = [f"message={self.message!r}"]
        if self.code:
            parts.append(f"code={self.code!r}")
        if self.details:
            parts.append(f"details={self.details!r}")
        return f"{self.__class__.__name__}({', '.join(parts)})"

to_dict()

Serialize the error for API responses.

Source code in website/exceptions/base.py
def to_dict(self):
    """Serialize the error for API responses."""
    result = {"error": self.message, "code": self.code}
    if self.details:
        result["details"] = self.details
    return result

UnauthorizedError

Bases: QuestMasterError

User not authorized to perform this action.

Source code in website/exceptions/base.py
class UnauthorizedError(QuestMasterError):
    """User not authorized to perform this action."""

    http_status = 403

    def __init__(self, message: str, user_id=None, action=None, **kwargs):
        kwargs.setdefault("code", "UNAUTHORIZED")
        details = kwargs.pop("details", {})
        if user_id is not None:
            details["user_id"] = user_id
        if action is not None:
            details["action"] = action
        super().__init__(message, details=details, **kwargs)

DuplicateRegistrationError

Bases: GameError

Player already registered for this game.

Source code in website/exceptions/business.py
class DuplicateRegistrationError(GameError):
    """Player already registered for this game."""

    def __init__(self, message: str, game_id=None, user_id=None, **kwargs):
        kwargs.setdefault("code", "DUPLICATE_REGISTRATION")
        details = kwargs.pop("details", {})
        if game_id is not None:
            details["game_id"] = game_id
        if user_id is not None:
            details["user_id"] = user_id
        super().__init__(message, details=details, **kwargs)

GameClosedError

Bases: GameError

Game is closed for registration.

Source code in website/exceptions/business.py
class GameClosedError(GameError):
    """Game is closed for registration."""

    def __init__(self, message: str, game_id=None, **kwargs):
        kwargs.setdefault("code", "GAME_CLOSED")
        details = kwargs.pop("details", {})
        if game_id is not None:
            details["game_id"] = game_id
        super().__init__(message, details=details, **kwargs)

GameError

Bases: QuestMasterError

Game-related business logic error.

Source code in website/exceptions/business.py
class GameError(QuestMasterError):
    """Game-related business logic error."""

    http_status = 409

    def __init__(self, message: str, **kwargs):
        kwargs.setdefault("code", "GAME_ERROR")
        super().__init__(message, **kwargs)

GameFullError

Bases: GameError

Game has reached maximum players.

Source code in website/exceptions/business.py
class GameFullError(GameError):
    """Game has reached maximum players."""

    def __init__(self, message: str, game_id=None, max_players=None, **kwargs):
        kwargs.setdefault("code", "GAME_FULL")
        details = kwargs.pop("details", {})
        if game_id is not None:
            details["game_id"] = game_id
        if max_players is not None:
            details["max_players"] = max_players
        super().__init__(message, details=details, **kwargs)

SessionConflictError

Bases: GameError

Game session overlaps with an existing session.

Source code in website/exceptions/business.py
class SessionConflictError(GameError):
    """Game session overlaps with an existing session."""

    def __init__(self, message: str, game_id=None, **kwargs):
        kwargs.setdefault("code", "SESSION_CONFLICT")
        details = kwargs.pop("details", {})
        if game_id is not None:
            details["game_id"] = game_id
        super().__init__(message, details=details, **kwargs)

DatabaseError

Bases: QuestMasterError

Database operation failed.

Source code in website/exceptions/database.py
class DatabaseError(QuestMasterError):
    """Database operation failed."""

    http_status = 500

    def __init__(self, message: str, operation=None, **kwargs):
        kwargs.setdefault("code", "DATABASE_ERROR")
        details = kwargs.pop("details", {})
        if operation is not None:
            details["operation"] = operation
        super().__init__(message, details=details, **kwargs)

DiscordAPIError

Bases: DiscordError

Discord API request failed.

Parameters:

Name Type Description Default
message str

Description of the API error.

required
status_code int

HTTP status code from the Discord API.

required
response dict

The raw response body from Discord (optional).

None
Source code in website/exceptions/discord.py
class DiscordAPIError(DiscordError):
    """Discord API request failed.

    Args:
        message: Description of the API error.
        status_code: HTTP status code from the Discord API.
        response: The raw response body from Discord (optional).
    """

    def __init__(self, message: str, status_code: int, response: dict = None):
        self.status_code = status_code
        self.response = response or {}
        super().__init__(
            message=f"[{status_code}] {message}",
            code=f"DISCORD_API_{status_code}",
            details={"status_code": status_code, "response": self.response},
        )

DiscordError

Bases: QuestMasterError

Base Discord-related error.

Source code in website/exceptions/discord.py
class DiscordError(QuestMasterError):
    """Base Discord-related error."""

    def __init__(self, message: str, **kwargs):
        kwargs.setdefault("code", "DISCORD_ERROR")
        super().__init__(message, **kwargs)

ValidationError

Bases: QuestMasterError

Input validation failed.

Parameters:

Name Type Description Default
message str

Description of the validation failure.

required
field str

The field that failed validation (optional).

None
code str

Machine-readable error code.

None
details dict

Additional context about the error.

None
Source code in website/exceptions/validation.py
class ValidationError(QuestMasterError):
    """Input validation failed.

    Args:
        message: Description of the validation failure.
        field: The field that failed validation (optional).
        code: Machine-readable error code.
        details: Additional context about the error.
    """

    http_status = 400

    def __init__(self, message: str, field: str = None, code: str = None, details: dict = None):
        self.field = field
        details = details or {}
        if field:
            details["field"] = field
        super().__init__(message=message, code=code or "VALIDATION_ERROR", details=details)