Error Handling
Context
Two recurring failure modes were discovered in this project:
- 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).
- 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.
Links
- Real implementation: code_source_simule/pipeline.py
- SMTP implementation: code_source_simule/flask_app.py
- Decision log: ../01_architecture/key-decisions.md — "Harden SMTP Alert Delivery"
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.