Aller au contenu

Error Handling

Context

Two recurring failure modes were discovered in this project:

  1. Silent failure — SMTP email alerts would fail silently. The business process continued, but operators had no idea an alert had been dropped. Discovered during production hardening (Mar 2026).
  2. Swallowed exceptions — Pipeline stages would catch exceptions without re-raising or logging them, making failures invisible until downstream data anomalies were noticed.

Both modes violate Constitution Principle IV: "Silent data loss, silent coercion, and swallowed exceptions are prohibited unless explicitly justified and tested."

Details

Fail-open with explicit logging (SMTP pattern)

The SMTP alert use case requires fail-open behavior: if email delivery fails, the business process must not be blocked. But the failure must be visible.

function send_alert(subject, body):
    try:
        connection = open_smtp_connection(
            host = SMTP_HOST,
            port = SMTP_PORT,
            mode = SMTP_MODE,   # "starttls" | "ssl" | "none"
            timeout = SMTP_TIMEOUT_SECONDS   # default: 10
        )
        connection.send(to=ALERT_RECIPIENTS, subject=subject, body=body)
        log_event("alert_sent", subject=subject, status="success")

    except SMTPException as error:
        log_event("alert_failed", subject=subject, error=str(error), status="failure")
        # Do NOT re-raise — fail-open: business process continues
        # But the failure is logged as a structured event (not silently swallowed)

Key distinction: fail-open ≠ silent failure. The error is always logged as a structured event with context, even if it does not block execution.

Explicit exception propagation (pipeline pattern)

For pipeline stages where failures SHOULD block execution (data integrity critical):

function run_pipeline_stage(stage_name, data):
    try:
        result = execute_stage(stage_name, data)
        log_event("stage_complete", stage=stage_name, rows_processed=len(result))
        return result

    except ValidationError as error:
        log_event("stage_failed", stage=stage_name, error=str(error), error_type="validation")
        raise   # Re-raise — caller decides whether to continue or halt

    except Exception as error:
        log_event("stage_failed", stage=stage_name, error=str(error), error_type="unexpected")
        raise   # Never swallow unexpected exceptions

Rule: Only catch exceptions you can meaningfully handle at that level. Always log before re-raising. Never use a bare except: pass.

Error context in logs

Every log entry for a failure MUST include: - stage or component — where the error occurred - error — the exception message (not just the type) - error_type — classification (validation, network, quota, unexpected) - timestamp — automatically added by the logger

This ensures errors are diagnosable without re-running the failure.

Examples

Debugging a silent failure: Search logs/ for structured JSON events with "status": "failure". Every failure logged via this pattern includes the full error context. If there is no log entry, the failure was swallowed — this is a bug to fix immediately.