# 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: 1. **`check-sensitive-logging.py`** blocks exception variables in `logger.warning/error/critical()` calls and forbids `exc_info=True` on production log levels. Only `logger.exception()` and `logger.debug(..., exc_info=True)` are permitted. 2. **`fix-exception-logging.py`** auto-removes exception variable references from f-strings and `exc_info=True` from non-debug logs. 3. **Intentional chain-breaking pattern** used across the codebase: catch a broad exception, log full details with `logger.exception()`, then raise a sanitized application exception *without* `from e`: ```python 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 only `error_code` and `message` to 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 e`** when the original exception is safe to propagate (no user data, internal infrastructure errors) - **Use `raise X from None`** when you want to explicitly suppress the chain and document the intent - **Omit `from`** when 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-logging` and `fix-exception-logging` hooks remain the primary defense against PII leakage through exceptions - `raise X from None` is available for cases where developers want to explicitly document chain suppression, but is not required