devnotes.

Custom Exceptions and Exception Chaining

Defining your own exception types, and preserving context when one error leads to another, both make error-handling code far more informative.

class InsufficientFundsError(Exception):        # inherit from Exception (or a subclass)
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Cannot withdraw {amount}, balance is only {balance}")

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    withdraw(100, 500)
except InsufficientFundsError as e:
    print(f"Failed: {e}")
    print(f"Shortfall: {e.amount - e.balance}")   # custom attributes are accessible

# Exception chaining — preserves the original cause
try:
    try:
        1 / 0
    except ZeroDivisionError as original:
        raise ValueError("Invalid calculation") from original   # chains the two errors
except ValueError as e:
    print(e)                 # "Invalid calculation"
    print(e.__cause__)        # the original ZeroDivisionError, still accessible

Custom exceptions, defined by inheriting from Exception (or a more specific built-in exception when appropriate), let calling code distinguish between different failure modes using except SpecificError: rather than parsing an error message string, and can carry extra data as attributes (e.balance, e.amount above) that a generic exception couldn't.

raise NewError(...) from original_error explicitly chains exceptions, recording that the new error arose while handling the original one — this preserves the full causal chain in tracebacks (shown as "The above exception was the direct cause of the following exception") rather than silently losing context about what actually triggered the failure, which matters enormously when debugging a production issue after the fact.

Custom Exceptions and Exception Chaining in Python | DevNotes