Custom Exceptions — When and How to Define Them
Think of custom exceptions as a structured error taxonomy, like a library's catalog system. Instead of rummaging through books (string messages) to guess their genre, you use specific categories and subcategories (exception types) to instantly identify and handle issues precisely.
The Setup
You are designing a Stripe payment integration library. If a payment fails, your library needs to raise exceptions. You want to distinguish between validation errors, network failures, and insufficient funds, so that users of your library can respond programmatically.
What Does This Print?
class PaymentError(Exception):
pass
def charge_card(amount, card_valid, funds_available):
if not card_valid:
raise PaymentError("Invalid card details")
if not funds_available:
raise PaymentError("Insufficient funds")
try:
charge_card(100, card_valid=True, funds_available=False)
except PaymentError as e:
# Consumers cannot easily differentiate reasons without string matching
if "Insufficient" in str(e):
print("Prompt user to add funds")
The Output
While string matching works, it introduces fragile dependencies. If a developer refactors the error message (e.g., changing 'Insufficient funds' to 'No balance'), the calling code will silently fail to handle the specific condition, likely crashing the application.
Why Python Does This
Python allows exceptions to be caught based on class inheritance. By subclassing your custom errors from a domain-specific base class, you create structured error trees. This leverages Python's method resolution order and exception-matching mechanisms, allowing developers to catch specific leaf node exceptions or fallback to general parent exceptions.
The Fix
class PaymentError(Exception): """Base exception for all payment failures."""
class InvalidCardError(PaymentError): """Raised when card validation fails."""
class InsufficientFundsError(PaymentError): """Raised when account balance is low."""
def charge_card(amount, card_valid, funds_available):
if not card_valid:
raise InvalidCardError("Card formatting error")
if not funds_available:
raise InsufficientFundsError("Balance too low")
Creating a custom exception hierarchy allows callers to catch specific, programmatic error types rather than parsing error strings. This provides a robust, future-proof mechanism for handling different error conditions, making the API clearer and less prone to breakage from internal message refactoring.
How This Fails in Real Systems
An automated trading system parsed brokers' API errors using string-matching. When the broker updated their API response message from 'No Liquid Capital' to 'Insufficient Collateral', the system's pattern matcher failed, bypassed safety guards, and kept placing invalid orders.