How to define an exception hierarchy for domain errors in Python

Create a custom exception hierarchy with a base DomainError class and specific subclasses to handle validation, not-found, permission, and concurrency errors cleanly in Python apps.

Easy Python 3.9+ Aug 9, 2026 Errors & debugging 14 views 0 copies

Python code

37 lines
Python 3.9+
class DomainError(Exception):
    """Base class for all domain errors."""
    pass

class ValidationError(DomainError):
    """Raised when input data fails validation rules."""
    pass

class NotFoundError(DomainError):
    """Raised when a requested entity does not exist."""
    pass

class PermissionDeniedError(DomainError):
    """Raised when a user lacks permission for an operation."""
    pass

class ConcurrencyError(DomainError):
    """Raised when a conflict occurs due to concurrent modifications."""
    pass

def process_order(order_id):
    if order_id <= 0:
        raise ValidationError(f"Invalid order ID: {order_id}")
    if order_id == 404:
        raise NotFoundError(f"Order {order_id} not found")
    if order_id == 403:
        raise PermissionDeniedError(f"Access denied for order {order_id}")
    if order_id == 409:
        raise ConcurrencyError(f"Order {order_id} was modified elsewhere")

if __name__ == "__main__":
    for order_id in [1, 404, 403, 409, -100]:
        try:
            process_order(order_id)
            print(f"Order {order_id}: processed successfully")
        except DomainError as e:
            print(f"Order {order_id}: {type(e).__name__} - {e}")

Output

stdout
Order 1: processed successfully
Order 404: NotFoundError - Order 404 not found
Order 403: PermissionDeniedError - Access denied for order 403
Order 409: ConcurrencyError - Order 409 was modified elsewhere
Order -100: ValidationError - Invalid order ID: -100

How it works

The DomainError base class subclasses built-in Exception, so all domain-specific errors share a common ancestor. Specific error types like ValidationError and NotFoundError inherit from DomainError, letting callers catch the whole family with one except DomainError clause while still distinguishing error types when needed. Each subclass uses pass because the behavior comes from the exception's type and message rather than extra properties. The process_order function raises the most specific exception that matches the failure condition, and the top-level loop demonstrates catching all domain errors uniformly. This pattern gives you a stable contract for error handling that survives refactoring.

Common mistakes

  • Raising `DomainError` directly instead of a specific subclass, losing precision about what went wrong
  • Using `except Exception` to catch domain errors, which also swallows unrelated bugs like `TypeError`
  • Forgetting to pass the error message to the parent constructor, so `str(e)` shows an empty string
  • Defining the hierarchy without a common base, forcing callers to handle each error type separately

Variations

  1. Add extra attributes to exception subclasses (e.g., `field_name` for `ValidationError`) by overriding `__init__` and calling `super().__init__`
  2. Use `Enum` or error codes alongside exceptions when clients need machine-readable identifiers

Real-world use cases

  • Handling user-facing API errors with specific 4xx responses based on exception type
  • Catching all business rule violations with one handler in a web framework while logging detailed type info
  • Enforcing a clean error contract across microservices where internal exceptions map to HTTP status codes

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Errors & debugging

Related tutorials and quizzes for this topic.