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.
Python code
37 linesclass 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
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
- Add extra attributes to exception subclasses (e.g., `field_name` for `ValidationError`) by overriding `__init__` and calling `super().__init__`
- 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
More from Errors & debugging
- Catch RecursionError and Fail Gracefully in Python easy
- Catch ValueError and print friendly message in Python easy
- Collect Multiple Validation Errors in Python Before Raising medium
- Handle ValueError and ZeroDivisionError in Python with try except easy
- How to Add a Correlation ID to Logging Records in Python medium
- How to Assert Preconditions with Descriptive Messages in Python easy
Keep learning
Related tutorials and quizzes for this topic.