Skip to content

Exception Hierarchy Pattern

Purpose

Exceptions control message flow in event-driven handlers. Instead of if/else chains, the exception type tells the handler how to dispose of the message.

The Hierarchy

Exception
├── RecoverableException       Requeue with exponential backoff
├── PermanentFailureException  Send to DLQ immediately, no retries
├── SilentSuccessException     Delete message, log as success
├── CheckpointUpdateError      Checkpoint persistence failed
├── DBTransactionError         Database operation failed (wraps original)
└── OperationalError (MySQL)   Visibility timeout + batch retry
    ├── Error 1213: Deadlock
    └── Error 1205: Lock wait timeout

Implementation

# core/exceptions.py

class RecoverableException(Exception):
    """Transient failure — retry with backoff. Message requeued to SQS."""
    def __init__(self, document_id: str, reason: str,
                 retry_count: int = 0, prev_page_count: int = 0):
        self.document_id = document_id
        self.reason = reason
        self.retry_count = retry_count
        self.prev_page_count = prev_page_count
        super().__init__(f"Recoverable error for {document_id}: {reason}")


class PermanentFailureException(Exception):
    """Unrecoverable failure — skip retries, send directly to DLQ."""
    def __init__(self, reason: str):
        self.reason = reason
        super().__init__(f"Permanent failure: {reason}")


class SilentSuccessException(Exception):
    """Not an error — processing should stop but message is successfully handled.
    Example: document already processed, DLQ redrive final pass."""
    def __init__(self, reason: str):
        self.reason = reason
        super().__init__(f"Silent success: {reason}")


class CheckpointUpdateError(Exception):
    """Checkpoint creation or update failed after retries."""
    pass


class DBTransactionError(Exception):
    """Database operation failed. Wraps the original exception with context."""
    def __init__(self, original_exception: Exception, context: str = ""):
        self.original_exception = original_exception
        self.context = context
        super().__init__(f"DB error ({context}): {original_exception}")

Handler Routing

# handlers/index.py

def _handle_exception(e: Exception, record: dict, batch_item_failures: list):
    message_id = record["messageId"]

    if isinstance(e, PermanentFailureException):
        log_message("error", f"Permanent failure: {e.reason}")
        send_to_dlq(record)  # Direct to DLQ, no retries

    elif isinstance(e, RecoverableException):
        log_message("warning", f"Recoverable: {e.reason}, retry #{e.retry_count}")
        handle_requeue_exception(record, e)  # Exponential backoff requeue

    elif isinstance(e, SilentSuccessException):
        log_message("info", f"Silent success: {e.reason}")
        # Message deleted automatically — no failure reported

    elif isinstance(e, OperationalError):
        log_message("warning", f"MySQL operational error: {e}")
        batch_item_failures.append({"itemIdentifier": message_id})
        change_message_visibility(record, 120)  # 2-minute cooldown

    else:
        log_message("error", f"Unexpected error: {e}", exc_info=True)
        batch_item_failures.append({"itemIdentifier": message_id})

When to Use Each Type

Exception Use When SQS Behavior
RecoverableException Transient failure (race condition, temporary resource unavailable) Requeue with exponential backoff
PermanentFailureException Data is invalid, cannot ever succeed Direct to DLQ
SilentSuccessException Already processed, or intentional skip Message deleted (success)
DBTransactionError Database write failed Depends on handler — usually batch retry
OperationalError MySQL deadlock or lock timeout Visibility timeout, batch retry

Key Insight

The exception hierarchy is the control flow contract between core/ and handlers/. Core code raises the appropriate exception type. The handler translates it into the correct SQS disposition. This keeps business logic clean — core/ doesn't know about SQS, DLQs, or visibility timeouts.

Ask the Architecture ×

Ask questions about Nextpoint architecture, patterns, rules, or any module. Powered by Claude Opus 4.6.