Skip to main content

Overview

Domain Exceptions are specialized error classes that represent business rule violations and domain-specific error conditions. They provide meaningful error messages and error codes that can be handled appropriately by the application layer.

Key Characteristics

  • Business-Focused: Represent domain rule violations, not technical errors
  • Descriptive: Provide clear, meaningful error messages
  • Error Codes: Include machine-readable error codes
  • Hierarchy: Organized in inheritance hierarchies
  • Self-Documenting: Exception names clearly describe the error
  • Type-Safe: Allow specific exception handling

Exception Structure

Domain exceptions in Soft-Bee API follow this pattern:
class DomainException(Exception):
    """Base exception for a domain"""
    def __init__(self, message: str, code: str = "DOMAIN_ERROR"):
        self.message = message
        self.code = code
        super().__init__(self.message)

class SpecificException(DomainException):
    """Specific domain exception"""
    def __init__(self, message: str = "Default error message"):
        super().__init__(message, "SPECIFIC_ERROR_CODE")

Real Example: Auth Exceptions

The auth feature demonstrates a complete exception hierarchy:
"""Excepciones específicas del dominio de autenticación"""

class AuthException(Exception):
    """Excepción base para el dominio de autenticación"""
    def __init__(self, message: str, code: str = "AUTH_ERROR"):
        self.message = message
        self.code = code
        super().__init__(self.message)

class InvalidEmailException(AuthException):
    """Email inválido"""
    def __init__(self, message: str = "Invalid email address"):
        super().__init__(message, "INVALID_EMAIL")

class WeakPasswordException(AuthException):
    """Password débil"""
    def __init__(self, message: str = "Password is too weak"):
        super().__init__(message, "WEAK_PASSWORD")

class InvalidUserException(AuthException):
    """Usuario inválido"""
    def __init__(self, message: str = "Invalid user data"):
        super().__init__(message, "INVALID_USER")

class UserNotFoundException(AuthException):
    """Usuario no encontrado"""
    def __init__(self, message: str = "User not found"):
        super().__init__(message, "USER_NOT_FOUND")

class InvalidCredentialsException(AuthException):
    """Credenciales inválidas"""
    def __init__(self, message: str = "Invalid credentials"):
        super().__init__(message, "INVALID_CREDENTIALS")

class AccountLockedException(AuthException):
    """Cuenta bloqueada"""
    def __init__(self, message: str = "Account is locked"):
        super().__init__(message, "ACCOUNT_LOCKED")

class TokenExpiredException(AuthException):
    """Token expirado"""
    def __init__(self, message: str = "Token has expired"):
        super().__init__(message, "TOKEN_EXPIRED")

class InvalidTokenException(AuthException):
    """Token inválido"""
    def __init__(self, message: str = "Invalid token"):
        super().__init__(message, "INVALID_TOKEN")

class EmailAlreadyExistsException(AuthException):
    """Email ya registrado"""
    def __init__(self, message: str = "Email already exists"):
        super().__init__(message, "EMAIL_EXISTS")
See the full implementation at src/features/auth/domain/exceptions/auth_exceptions.py.

Exception Design Principles

1. Base Exception Per Domain

Create a base exception for each domain:
class AuthException(Exception):
    """Base for all auth-related exceptions"""
    def __init__(self, message: str, code: str = "AUTH_ERROR"):
        self.message = message
        self.code = code
        super().__init__(self.message)

2. Specific Exceptions

Create specific exceptions for each error type:
class InvalidEmailException(AuthException):
    def __init__(self, message: str = "Invalid email address"):
        super().__init__(message, "INVALID_EMAIL")

3. Error Codes

Use consistent, uppercase error codes:
code: str = "INVALID_EMAIL"        # Good
code: str = "USER_NOT_FOUND"       # Good
code: str = "ACCOUNT_LOCKED"       # Good

code: str = "error1"               # Avoid
code: str = "InvalidEmail"         # Avoid

4. Default Messages

Provide sensible default messages:
class UserNotFoundException(AuthException):
    def __init__(self, message: str = "User not found"):
        super().__init__(message, "USER_NOT_FOUND")

# Can use default
raise UserNotFoundException()

# Or customize
raise UserNotFoundException(f"User {user_id} not found")

5. Inheritance Hierarchy

Organize exceptions in logical hierarchies:
AuthException                    # Base
├── InvalidEmailException        # Validation
├── WeakPasswordException        # Validation
├── InvalidUserException         # Validation
├── UserNotFoundException        # Not Found
├── InvalidCredentialsException  # Authentication
├── AccountLockedException       # Authorization
├── TokenExpiredException        # Token
└── InvalidTokenException        # Token

Using Exceptions in Domain Layer

In Value Objects

Validate and throw exceptions:
@dataclass(frozen=True)
class Email:
    value: str
    
    def __post_init__(self):
        if not self.is_valid():
            raise InvalidEmailException(f"Invalid email address: {self.value}")
See src/features/auth/domain/value_objects/email.py:12.

In Entities

Validate business rules:
@dataclass
class User:
    def validate(self):
        if len(self.username) < 3:
            raise InvalidUserException("Username must be at least 3 characters")
        
        if not self.email.is_valid():
            raise InvalidUserException("Invalid email address")
See src/features/auth/domain/entities/user.py:31.

In Application Services

Handle domain logic errors:
class LoginUseCase:
    def execute(self, email: str, password: str) -> Token:
        user = self.user_repository.find_by_email(email)
        if not user:
            raise UserNotFoundException(f"No user found with email {email}")
        
        if user.is_locked():
            raise AccountLockedException(
                f"Account locked after {user.failed_login_attempts} failed attempts"
            )
        
        if not self.password_service.verify(password, user.hashed_password):
            user.login_failed()
            self.user_repository.save(user)
            raise InvalidCredentialsException("Incorrect password")
        
        user.login_successful()
        return self.token_service.create_token(user)

Creating New Domain Exceptions

Step 1: Create Base Exception

# src/features/hives/domain/exceptions/hive_exceptions.py

class HiveException(Exception):
    """Base exception for hive domain"""
    def __init__(self, message: str, code: str = "HIVE_ERROR"):
        self.message = message
        self.code = code
        super().__init__(self.message)

Step 2: Define Specific Exceptions

class InvalidHiveException(HiveException):
    """Invalid hive data"""
    def __init__(self, message: str = "Invalid hive data"):
        super().__init__(message, "INVALID_HIVE")

class HiveNotFoundException(HiveException):
    """Hive not found"""
    def __init__(self, message: str = "Hive not found"):
        super().__init__(message, "HIVE_NOT_FOUND")

class InvalidLocationException(HiveException):
    """Invalid hive location"""
    def __init__(self, message: str = "Invalid location"):
        super().__init__(message, "INVALID_LOCATION")

class HiveInactiveException(HiveException):
    """Operation on inactive hive"""
    def __init__(self, message: str = "Cannot perform operation on inactive hive"):
        super().__init__(message, "HIVE_INACTIVE")

class InspectionException(HiveException):
    """Base for inspection-related errors"""
    def __init__(self, message: str, code: str = "INSPECTION_ERROR"):
        super().__init__(message, code)

class InspectionNotFoundException(InspectionException):
    """Inspection not found"""
    def __init__(self, message: str = "Inspection not found"):
        super().__init__(message, "INSPECTION_NOT_FOUND")

class InvalidInspectionDateException(InspectionException):
    """Invalid inspection date"""
    def __init__(self, message: str = "Invalid inspection date"):
        super().__init__(message, "INVALID_INSPECTION_DATE")

class UnauthorizedAccessException(HiveException):
    """Unauthorized access to hive"""
    def __init__(self, message: str = "Unauthorized access to hive"):
        super().__init__(message, "UNAUTHORIZED_ACCESS")

Step 3: Use in Domain Logic

# In Hive entity
def validate(self):
    if not self.name or len(self.name) < 2:
        raise InvalidHiveException("Hive name must be at least 2 characters")
    if not self.beekeeper_id:
        raise InvalidHiveException("Hive must belong to a beekeeper")

# In HiveLocation value object
def validate(self):
    if not -90 <= self.latitude <= 90:
        raise InvalidLocationException("Latitude must be between -90 and 90")
    if not -180 <= self.longitude <= 180:
        raise InvalidLocationException("Longitude must be between -180 and 180")

# In application service
def get_hive(self, hive_id: str, user_id: str) -> Hive:
    hive = self.hive_repository.find_by_id(hive_id)
    if not hive:
        raise HiveNotFoundException(f"Hive {hive_id} not found")
    
    if hive.beekeeper_id != user_id:
        raise UnauthorizedAccessException(
            f"User {user_id} does not have access to hive {hive_id}"
        )
    
    return hive

Exception Categories

Validation Exceptions

Invalid input or data:
class InvalidEmailException(AuthException)
class WeakPasswordException(AuthException)
class InvalidHiveException(HiveException)
class InvalidLocationException(HiveException)

Not Found Exceptions

Requested resource doesn’t exist:
class UserNotFoundException(AuthException)
class HiveNotFoundException(HiveException)
class InspectionNotFoundException(InspectionException)

Business Rule Exceptions

Business logic violations:
class AccountLockedException(AuthException)
class HiveInactiveException(HiveException)
class EmailAlreadyExistsException(AuthException)

Authorization Exceptions

Permission or access violations:
class UnauthorizedAccessException(HiveException)
class InvalidCredentialsException(AuthException)

State Exceptions

Invalid state transitions:
class InvalidStateTransitionException(HiveException)
class OperationNotAllowedException(HiveException)

Best Practices

Use Descriptive Names

# Good
class UserNotFoundException(AuthException)
class WeakPasswordException(AuthException)
class AccountLockedException(AuthException)

# Avoid
class Error1(AuthException)
class BadInput(AuthException)
class Failed(AuthException)

Provide Context in Messages

# Good
raise UserNotFoundException(f"User with email {email} not found")
raise InvalidLocationException(f"Latitude {lat} must be between -90 and 90")

# Avoid
raise UserNotFoundException("Not found")
raise InvalidLocationException("Invalid")

Use Consistent Error Codes

# Good - Following a pattern
"USER_NOT_FOUND"
"HIVE_NOT_FOUND"
"INSPECTION_NOT_FOUND"

"INVALID_EMAIL"
"INVALID_HIVE"
"INVALID_LOCATION"

# Avoid - Inconsistent
"userNotFound"
"hive-not-found"
"INSPECTIONNOTFOUND"

Catch Specific Exceptions

# Good
try:
    user = user_service.create_user(email, password)
except InvalidEmailException as e:
    return {"error": e.message, "code": e.code}, 400
except WeakPasswordException as e:
    return {"error": e.message, "code": e.code}, 400
except EmailAlreadyExistsException as e:
    return {"error": e.message, "code": e.code}, 409

# Avoid - Catching base exception
try:
    user = user_service.create_user(email, password)
except AuthException as e:
    return {"error": e.message}, 400  # Lost specificity

Don’t Swallow Exceptions

# Good
try:
    hive = hive_repository.find_by_id(hive_id)
    if not hive:
        raise HiveNotFoundException(f"Hive {hive_id} not found")
except HiveNotFoundException:
    raise  # Re-raise or handle appropriately

# Avoid
try:
    hive = hive_repository.find_by_id(hive_id)
except Exception:
    pass  # Silently swallowing errors

Include Additional Context When Useful

class ValidationException(DomainException):
    """Validation error with field information"""
    def __init__(self, message: str, field: str = None):
        super().__init__(message, "VALIDATION_ERROR")
        self.field = field

# Usage
raise ValidationException("Email is required", field="email")
raise ValidationException("Password too short", field="password")

Handling Exceptions in Presentation Layer

Map domain exceptions to HTTP responses:
from flask import jsonify

@app.errorhandler(UserNotFoundException)
def handle_user_not_found(e):
    return jsonify({
        "error": e.message,
        "code": e.code
    }), 404

@app.errorhandler(InvalidEmailException)
def handle_invalid_email(e):
    return jsonify({
        "error": e.message,
        "code": e.code
    }), 400

@app.errorhandler(AccountLockedException)
def handle_account_locked(e):
    return jsonify({
        "error": e.message,
        "code": e.code
    }), 403

Build docs developers (and LLMs) love