3.5 KiB
ADR-0003: Reject universal raise-without-from enforcement
Date: 2026-03-28 Status: Accepted
Context
PR #3225 proposed a pre-commit hook (check-raise-without-from) to enforce
raise X(...) from e inside all except blocks, citing PEP 3134 (explicit
exception chaining). The hook would flag any raise NewException(...) inside
an except handler that omits the from clause.
The conflict with existing security hooks
The codebase already enforces exception sanitization to prevent PII leakage through three mechanisms:
-
check-sensitive-logging.pyblocks exception variables inlogger.warning/error/critical()calls and forbidsexc_info=Trueon production log levels. Onlylogger.exception()andlogger.debug(..., exc_info=True)are permitted. -
fix-exception-logging.pyauto-removes exception variable references from f-strings andexc_info=Truefrom non-debug logs. -
Intentional chain-breaking pattern used across the codebase: catch a broad exception, log full details with
logger.exception(), then raise a sanitized application exception withoutfrom e:except Exception as e: logger.exception("Error getting news feed") # full details logged raise NewsFeedGenerationException(str(e), ...) # no "from e"This pattern appears in
news/api.py,notifications/service.py,utilities/es_utils.py, and others. Flask error handlers then return onlyerror_codeandmessageto the client — never stack traces.
Why raise X from e is harmful here
raise X from e preserves the full original exception chain at the Python
runtime level. Even if logs are sanitized, the chain propagates to:
- Error tracking services (Sentry, DataDog) that capture
__cause__ - Any downstream handler that logs with
exc_info=True - Test output and development tooling that prints full tracebacks
If the original exception contains PII (SQL errors with user data, API responses with auth tokens, file paths with usernames), preserving the chain defeats the sanitization strategy.
When from e IS appropriate
Explicit chaining is valuable in infrastructure code where:
- The original exception contains no user data (e.g., config parsing errors)
- Both exceptions will be caught and handled before reaching any boundary
- Debugging requires understanding the root cause chain
The seven existing from e usages in the codebase (mcp/client.py,
config/llm_config.py, security/path_validator.py, exporters/) follow
this pattern correctly.
Decision
Do not add a universal raise-without-from enforcement hook. The existing
pattern of intentional chain-breaking for PII protection takes precedence
over PEP 3134 compliance.
Developers should use their judgment:
- Use
raise X from ewhen the original exception is safe to propagate (no user data, internal infrastructure errors) - Use
raise X from Nonewhen you want to explicitly suppress the chain and document the intent - Omit
fromwhen wrapping user-facing exceptions to sanitize error details (the current default pattern)
Consequences
- The allowlisted files in the rejected PR do not need remediation
- Exception chain decisions remain a code review concern, not an automated enforcement
- The existing
check-sensitive-loggingandfix-exception-logginghooks remain the primary defense against PII leakage through exceptions raise X from Noneis available for cases where developers want to explicitly document chain suppression, but is not required