The Custom Alarm Codes: Creating Your Own Exceptions

Timothy had mastered catching and handling exceptions, but he faced a new challenge. The library’s cataloging system raised generic ValueError and KeyError exceptions for dozens of different problems. When reviewing error logs, he couldn’t distinguish between a missing ISBN, an invalid publication date, a malformed author name, or a duplicate catalog entry—they all looked the same.

Margaret led him to the bell tower, where each alarm bell bore an engraved label: “Fire,” “Intruder,” “Flood,” “Roof Collapse.” “Generic exceptions,” she explained, “are like ringing any available bell. Custom exceptions are like having a specific bell for each type of emergency, so responders know exactly what they’re dealing with.”

The Generic Exception Problem

Timothy’s catalog validation raised built-in exceptions:

# Note: Examples use placeholder functions like request_isbn_correction(), fix_isbn()
# In practice, replace these with your actual implementation

def validate_book_data(book):
    if 'isbn' not in book:
        raise ValueError("Missing ISBN")

    if len(book['isbn']) != 13:
        raise ValueError("ISBN must be 13 digits")

    if book['year'] < 1000 or book['year'] > 2100:
        raise ValueError("Invalid publication year")

    if not book['title']:
        raise ValueError("Title cannot be empty")

Every validation problem raised ValueError. When catching these exceptions, Timothy couldn’t distinguish between them:

try:
    validate_book_data(book)
except ValueError as e:
    # Which validation failed? We have to parse the message string!
    if "ISBN" in str(e):
        fix_isbn_problem()
    elif "year" in str(e):
        fix_year_problem()
    # This is fragile and ugly

Parsing error messages to determine the problem was brittle. Margaret showed him a better way.

Creating Basic Custom Exceptions

“Custom exceptions,” Margaret explained, “are simply classes that inherit from Exception or one of its subclasses.”

class CatalogError(Exception):
    """Base exception for catalog-related errors"""
    pass

class InvalidISBNError(CatalogError):
    """Raised when ISBN format is invalid"""
    pass

class InvalidYearError(CatalogError):
    """Raised when publication year is invalid"""
    pass

class MissingFieldError(CatalogError):
    """Raised when required field is missing"""
    pass

Now Timothy could raise specific exceptions:

def validate_book_data(book):
    if 'isbn' not in book:
        raise MissingFieldError("ISBN is required")

    if len(book['isbn']) != 13:
        raise InvalidISBNError(f"ISBN must be 13 digits, got {len(book['isbn'])}")

    if book['year'] < 1000 or book['year'] > 2100:
        raise InvalidYearError(f"Year {book['year']} is outside valid range")

    if not book['title']:
        raise MissingFieldError("Title is required")

And catch them specifically:

try:
    validate_book_data(book)
except InvalidISBNError as e:
    # Handle ISBN problems
    book['isbn'] = request_isbn_correction()
except InvalidYearError as e:
    # Handle year problems
    book['year'] = estimate_publication_year(book)
except MissingFieldError as e:
    # Handle missing fields
    skip_incomplete_record(book)

Each exception type could have its own handling logic, without parsing error messages.

The Critical Rule: Inherit from Exception

Margaret showed Timothy an important warning in the exception guidelines:

# WRONG - Never do this!
class CatalogError(BaseException):  # ✗ Bad!
    """Don't inherit from BaseException directly"""
    pass

# RIGHT - Always inherit from Exception
class CatalogError(Exception):  # ✓ Good!
    """Base exception for catalog operations"""
    pass

“Why does this matter?” Timothy asked.

“Because BaseException is the root of all exceptions,” Margaret explained, “including system exceptions like SystemExit, KeyboardInterrupt, and GeneratorExit. If you inherit from BaseException, your exceptions bypass normal exception handling.”

class BadCatalogError(BaseException):
    pass

try:
    raise BadCatalogError("Problem!")
except Exception:
    print("Caught it!")  # This never executes!

# The exception propagates because Exception doesn't catch BaseException children

“Even worse,” Margaret continued, “you might accidentally catch system signals:”

try:
    long_operation()
except BadCatalogError:  # Meant to catch catalog errors
    cleanup()

# If someone presses Ctrl+C and your code raises SystemExit,
# this won't catch it as expected because of the inheritance confusion

“Always inherit from Exception or one of its subclasses like ValueError, RuntimeError, or TypeError,” Margaret emphasized. “Reserve BaseException for the Python internals.”

The Exception Hierarchy

Margaret showed Timothy the power of exception hierarchies:

class CatalogError(Exception):
    """Base class for all catalog exceptions"""
    pass

class ValidationError(CatalogError):
    """Base class for validation errors"""
    pass

class InvalidISBNError(ValidationError):
    """ISBN validation failed"""
    pass

class InvalidYearError(ValidationError):
    """Year validation failed"""
    pass

class StorageError(CatalogError):
    """Base class for storage errors"""
    pass

class DuplicateEntryError(StorageError):
    """Entry already exists in catalog"""
    pass

class CatalogFullError(StorageError):
    """Catalog storage is full"""
    pass

The hierarchy allowed catching at different levels of specificity:

try:
    process_book(book)
except InvalidISBNError:
    # Handle this specific problem
    fix_isbn(book)
except ValidationError:
    # Catch any other validation error
    log_validation_failure(book)
except StorageError:
    # Catch any storage problem
    retry_with_different_storage()
except CatalogError:
    # Catch any catalog-related error
    alert_administrator()

“The hierarchy,” Margaret explained, “lets you handle problems at the appropriate level of granularity. Specific when you need it, general when you don’t.”

Adding Context to Exceptions

Timothy discovered he could add data to exceptions:

class DuplicateEntryError(CatalogError):
    """Raised when attempting to add duplicate entry"""

    def __init__(self, isbn, existing_id, message=None):
        self.isbn = isbn
        self.existing_id = existing_id
        if message is None:
            message = f"Book with ISBN {isbn} already exists as entry {existing_id}"
        super().__init__(message)

# Raise with context
def add_book_to_catalog(book):
    if book['isbn'] in catalog:
        existing_id = catalog[book['isbn']]['id']
        raise DuplicateEntryError(book['isbn'], existing_id)

    # Add book...

# Access context when handling
try:
    add_book_to_catalog(book)
except DuplicateEntryError as e:
    print(f"ISBN {e.isbn} conflicts with entry {e.existing_id}")
    merge_with_existing_entry(e.existing_id, book)

The exception carried structured data, not just a message. Handlers could access this data programmatically.

When to Create Custom Exceptions

Margaret compiled guidelines for custom exceptions:

Create custom exceptions when:

# 1. You need to distinguish between similar errors
class ISBNFormatError(ValidationError):
    pass

class ISBNChecksumError(ValidationError):
    pass

# Handlers can distinguish these, even though both are ISBN problems

# 2. You need to attach context data
class QuotaExceededError(CatalogError):
    def __init__(self, user_id, current_count, limit):
        self.user_id = user_id
        self.current_count = current_count
        self.limit = limit
        super().__init__(
            f"User {user_id} exceeded quota: {current_count}/{limit}"
        )

# 3. Your API needs a public exception interface
class LibraryAPIError(Exception):
    """Users of your library will catch this"""
    pass

# Your library raises specific subclasses, users catch the base

Use built-in exceptions when:

# 1. A built-in exception fits perfectly
def get_book(book_id):
    if book_id not in catalog:
        raise KeyError(book_id)  # KeyError is perfect for "key not found"

# 2. The error is truly generic
def calculate_average_pages(books):
    if not books:
        raise ValueError("Cannot calculate average of empty list")

# 3. You're following Python conventions
def __getitem__(self, index):
    if index >= len(self.books):
        raise IndexError("Index out of range")  # Expected by Python

Naming Conventions

Margaret emphasized naming best practices:

# Good names - describe what went wrong
class InvalidISBNError(Exception):
    pass

class CatalogNotFoundError(Exception):
    pass

class DuplicateEntryError(Exception):
    pass

# Poor names - vague or misleading
class ISBNException(Exception):  # "Exception" suffix is redundant
    pass

class Error(Exception):  # Too generic
    pass

class CatalogProblem(Exception):  # Not clear this is an exception
    pass

“Custom exceptions should end with ‘Error,'” Margaret noted, “unless you have a compelling reason otherwise. And the name should clearly indicate what went wrong.”

Documentation Matters

Timothy learned that documenting custom exceptions was critical:

class InvalidCatalogFormatError(CatalogError):
    """Raised when catalog file format is invalid or corrupted.

    This exception indicates the catalog file exists but cannot be parsed.
    This is distinct from CatalogNotFoundError (file missing) and 
    CatalogPermissionError (file unreadable).

    Attributes:
        filename (str): Path to the invalid catalog file
        line_number (int): Line where parsing failed, or None if unknown
        details (str): Additional information about the format problem
    """

    def __init__(self, filename, line_number=None, details=None):
        self.filename = filename
        self.line_number = line_number
        self.details = details

        message = f"Invalid catalog format in {filename}"
        if line_number:
            message += f" at line {line_number}"
        if details:
            message += f": {details}"

        super().__init__(message)

The docstring explained when to raise this exception, how it differed from related exceptions, and documented all custom attributes.

Exception String Representation

Margaret showed Timothy how to customize error messages:

class BookConflictError(CatalogError):
    """Raised when books conflict in catalog"""

    def __init__(self, book1_id, book2_id, conflict_reason):
        self.book1_id = book1_id
        self.book2_id = book2_id
        self.conflict_reason = conflict_reason
        super().__init__(self._format_message())

    def _format_message(self):
        return (
            f"Conflict between books {self.book1_id} and {self.book2_id}: "
            f"{self.conflict_reason}"
        )

    def __str__(self):
        # Custom string representation for display
        return (
            f"Book Conflict Errorn"
            f"  Book 1: {self.book1_id}n"
            f"  Book 2: {self.book2_id}n"
            f"  Reason: {self.conflict_reason}"
        )

The exception provided both a standard message and a formatted display version.

The Don’t Over-Engineer Principle

Margaret cautioned Timothy about exception proliferation:

# Over-engineered - too many specific exceptions
class ISBNTooShortError(ValidationError):
    pass

class ISBNTooLongError(ValidationError):
    pass

class ISBNContainsLettersError(ValidationError):
    pass

class ISBNContainsSpacesError(ValidationError):
    pass

# Better - one exception with details
class InvalidISBNError(ValidationError):
    def __init__(self, isbn, reason):
        self.isbn = isbn
        self.reason = reason
        super().__init__(f"Invalid ISBN {isbn}: {reason}")

“Don’t create a new exception class,” Margaret advised, “unless you need to handle it differently. Use attributes to convey details instead.”

Real-World Example: API Error Hierarchy

Timothy studied a professional exception design:

class LibraryAPIError(Exception):
    """Base exception for the library API"""
    pass

class AuthenticationError(LibraryAPIError):
    """Authentication failed"""
    pass

class AuthorizationError(LibraryAPIError):
    """User lacks required permissions"""
    def __init__(self, user_id, required_permission):
        self.user_id = user_id
        self.required_permission = required_permission
        super().__init__(
            f"User {user_id} lacks permission: {required_permission}"
        )

class ResourceNotFoundError(LibraryAPIError):
    """Requested resource doesn't exist"""
    def __init__(self, resource_type, resource_id):
        self.resource_type = resource_type
        self.resource_id = resource_id
        super().__init__(
            f"{resource_type} {resource_id} not found"
        )

class RateLimitError(LibraryAPIError):
    """Rate limit exceeded"""
    def __init__(self, retry_after):
        self.retry_after = retry_after
        super().__init__(
            f"Rate limit exceeded, retry after {retry_after} seconds"
        )

The hierarchy provided a clean API contract—clients could catch LibraryAPIError to handle any API problem, or catch specific errors for precise handling.

Timothy’s Custom Exception Wisdom

Through mastering the Custom Alarm Codes, Timothy learned essential principles:

Always inherit from Exception: Never inherit from BaseException directly—it includes system exceptions like SystemExit and KeyboardInterrupt.

Create exception hierarchies: Group related exceptions under a base class for flexible catching.

End names with “Error”: Follow Python conventions for exception naming.

Add useful attributes: Include structured data that handlers can use programmatically.

Document thoroughly: Explain when to raise, how it differs from similar exceptions, and document attributes.

Don’t over-engineer: Create new exception types only when you need different handling.

Provide good messages: Make error messages helpful for debugging.

Follow conventions: Use built-in exceptions when they fit, custom ones for domain-specific errors.

Design for your API: Custom exceptions are part of your public interface.

Timothy’s mastery of custom exceptions transformed error handling from generic message parsing into a structured, type-safe system. The Custom Alarm Codes ensured that when something went wrong, everyone knew exactly what type of emergency had occurred and could respond appropriately. Each bell in the tower rang with its unique pattern, summoning the right responders with the right tools to address each specific problem.

Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Similar Posts