Designing Custom Exceptions

Building semantic error hierarchies for professional libraries. From Inheritance to Chaining.

1. The Big Idea (ELI5)

Pro-Tip

Imagine driving a car. The dashboard lights up.

  • Standard Error (`Exception`): A generic red light just says "ERROR". Is it the engine? The tire? The door? You don't know. You have to stop everything.
  • Custom Exception (`EngineOverheatError`): A specific symbol lights up. You know exacty what's wrong. You can decide if you need to stop immediately or if you can drive to the nearest garage.
  • The Goal: Custom exceptions give the caller of your code the power to make specific decisions based on exactly what failed.

2. The Syntax: Inheriting from Exception

A custom exception is just a normal class that inherits from `Exception`. You can add method, properties, and custom logic to it.

python
class NegativeBalanceError(Exception):
    """Raised when a withdrawal is larger than the balance."""
    
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        # Pass a nice message to the parent class
        super().__init__(f"Cannot withdraw {amount}. Balance is only {balance}.")

# Usage
try:
    raise NegativeBalanceError(100, 500)
except NegativeBalanceError as e:
    print(e)             # Cannot withdraw 500. Balance is only 100.
    print(e.balance)     # 100 (We can access the data programmatically!)

3. Best Practice: The Library Base Exception

When writing a library (e.g., `requests`, `pandas`), you should never let your internal implementation details leak out.Always create a base exception class for your project.

python
# 1. The Base Class
class FileFusionError(Exception):
    """Base class for all exceptions in this module."""
    pass

# 2. Specific Errors inherit from the Base
class ConnectionError(FileFusionError):
    pass

class AuthError(FileFusionError):
    pass

# 3. Usage for the User
try:
    file_fusion.connect()
except FileFusionError:
    # Catches ConnectionError, AuthError, and any future errors
    # WITHOUT catching ValueError (which might be a bug in their own code)
    print("Something went wrong with FileFusion!")

Pro-Tip

Anti-Pattern: Never inherit from `BaseException`. It skips the normal exception hierarchy and might prevent users from stopping your code with Ctrl+C.

4. Deep Dive: Exception Chaining (`raise from`)

Sometimes you catch an error (like `KeyError`) but want to re-raise it as a semantic error (like `ConfigError`). You must preserve the original traceback so debugging isn't impossible.

python
class ConfigError(Exception):
    pass

def load_config(key):
    try:
        data = {"host": "localhost"}
        return data[key]
    except KeyError as e:
        #  Bad: raise ConfigError("Missing Key") -> Loses the original error
        
        # Good: Chain the exception
        raise ConfigError(f"Configuration missing: {key}") from e

# Traceback will show:
# KeyError: 'port'
# The above exception was the direct cause of the following exception:
# ConfigError: Configuration missing: port

The `from e` syntax attaches the original exception to the `__cause__` attribute of the new exception. Similary, explicit suppression uses `from None`.

5. Attaching Payload and State

Exceptions are objects. Use this! Instead of parsing error string messages (flaky), attach structured data to the exception instance.

python
class APILimitError(Exception):
    def __init__(self, limit, reset_time):
        self.limit = limit
        self.reset_time = reset_time # Unix timestamp
        super().__init__(f"Rate limit of {limit} exceeded.")

try:
    call_api()
except APILimitError as e:
    import time
    wait_time = e.reset_time - time.time()
    print(f"Sleeping for {wait_time} seconds...")
    time.sleep(wait_time)

6. When NOT to use Custom Exceptions

Don't reinvent the wheel. If a built-in exception fits, use it.

  • If a function gets the wrong argument type? Use `TypeError`.
  • If the value is invalid (e.g., -1 for age)? Use `ValueError`.
  • If a file is missing? Use `FileNotFoundError`.

Only create custom exceptions when the caller needs to distinguish this specific error from others programmatically.