<!-- AUTO-GENERATED — do not edit this file directly.
     Source of truth: conformance/suite/rules/error_handling.py
     To regenerate:  uv run atlan-application-sdk-conformance gen-rule-docs
     To check CI staleness: uv run atlan-application-sdk-conformance gen-rule-docs --check -->

# Error-Handling Rules (E-series)

**20 rules** · Checker: `suite.checks.error_handling` (AST-based)

Suppress a finding on the violating line or the line directly above it:

```python
# conformance: ignore[E012] intentional: stdlib interop
```

| ID | Name | Tier | Scope | Category | Autofixable | Since |
|---|---|---|---|---|---|---|
| [E001](#e001) | `BareExceptPass` | `block` | `both` | `silent-swallow` | — | 0.2.0 |
| [E002](#e002) | `TypedExceptPass` | `block` | `both` | `silent-swallow` | — | 0.2.0 |
| [E003](#e003) | `BroadContextlibSuppress` | `warn` | `both` | `silent-swallow` | — | 0.2.0 |
| [E004](#e004) | `BroadExceptClause` | `warn` | `both` | `overly-broad-catch` | — | 0.2.0 |
| [E005](#e005) | `ExceptBlockMissingExcInfo` | `warn` | `both` | `missing-traceback` | yes | 0.2.0 |
| [E006](#e006) | `BareExceptWithBody` | `block` | `both` | `silent-swallow` | — | 0.2.0 |
| [E007](#e007) | `ErrorToReturnValue` | `warn` | `both` | `error-to-return-value` | — | 0.2.0 |
| [E008](#e008) | `ImportErrorWithoutLogging` | `warn` | `both` | `optional-import` | — | 0.2.0 |
| [E009](#e009) | `ExceptBlockOnlyAssigns` | `warn` | `both` | `error-to-return-value` | — | 0.2.0 |
| [E010](#e010) | `AsyncioGatherExceptionsUnexamined` | `warn` | `both` | `asyncio-unexamined` | — | 0.2.0 |
| [E011](#e011) | `LoggingFilterUnsafeBody` | `warn` | `both` | `filter-safety` | — | 0.2.0 |
| [E012](#e012) | `UntypedBuiltinRaise` | `warn` | `both` | `untyped-raise` | — | 0.2.0 |
| [E013](#e013) | `LegacyAtlanErrorRaise` | `block` | `both` | `legacy-raise` | — | 0.2.0 |
| [E014](#e014) | `ExceptLoopControlSwallow` | `warn` | `both` | `silent-swallow` | — | 0.2.0 |
| [E015](#e015) | `ExceptionTextInErrorMessage` | `warn` | `both` | `error-message-hygiene` | — | 0.2.0 |
| [E016](#e016) | `MissingExceptionChaining` | `warn` | `both` | `exception-chaining` | yes | 0.2.0 |
| [E017](#e017) | `SecretNamedEvidenceKey` | `block` | `both` | `security` | — | 0.2.0 |
| [E018](#e018) | `BareParentLeafRaise` | `warn` | `both` | `untyped-raise` | — | 0.2.0 |
| [E019](#e019) | `ExceptionTextInContractField` | `warn` | `both` | `error-message-hygiene` | — | 0.9.0 |
| [E020](#e020) | `HttpFailureToEmptyReturn` | `warn` | `app` | `error-to-return-value` | — | 0.9.0 |

---

## E001 — `BareExceptPass` {#e001}

**Tier:** `block` · **Scope:** `both` · **Category:** `silent-swallow` · **Autofixable:** — · **Since:** 0.2.0

> Bare 'except: pass' silently discards every exception

**Rationale:** The hardest class of production bug to debug: no stack trace, no log record, no
indication anything failed. Every downstream anomaly (wrong results, missing records) is
diagnosed with no artifact pointing back to the origin.

A bare `except: pass` catches KeyboardInterrupt, SystemExit, and GeneratorExit and
discards them with no trace.  This is the hardest class of bugs to debug.  Replace with
a typed catch that at minimum logs the error with `exc_info=True`.  Never acceptable —
even cleanup paths should log at DEBUG.

---

## E002 — `TypedExceptPass` {#e002}

**Tier:** `block` · **Scope:** `both` · **Category:** `silent-swallow` · **Autofixable:** — · **Since:** 0.2.0

> Typed 'except SomeError: pass' discards exception silently

**Rationale:** A typed catch that discards silently still destroys the event record. Stack traces at
the point of failure are often the only artifact that survives async and service
boundaries in a distributed system.

A typed catch that still discards silently loses the stack trace entirely. Acceptable
only for truly trivial best-effort operations where failure is 100% expected AND the
surrounding code handles the missing result, AND there is a comment explaining the
reasoning.

---

## E003 — `BroadContextlibSuppress` {#e003}

**Tier:** `warn` · **Scope:** `both` · **Category:** `silent-swallow` · **Autofixable:** — · **Since:** 0.2.0

> contextlib.suppress() — check whether scope is too broad

**Rationale:** suppress(Exception) is semantically identical to except Exception: pass — it absorbs
every unexpected failure with no trace. A narrow suppress(FileNotFoundError) is safe;
anything broader is a hidden failure sink.

`contextlib.suppress(Exception)` or `suppress(BaseException)` is HIGH severity; narrow
`suppress(FileNotFoundError)` on a cleanup path is acceptable.  The checker must inspect
the suppressed exception type before classifying.

---

## E004 — `BroadExceptClause` {#e004}

**Tier:** `warn` · **Scope:** `both` · **Category:** `overly-broad-catch` · **Autofixable:** — · **Since:** 0.2.0

> Overly broad 'except Exception/BaseException' without exc_info

**Rationale:** Without exc_info, the stack trace is gone at the point of capture. A broad catch without
a traceback also masks completely unexpected exceptions from upstream code, making
root-cause analysis impossible.

Catches everything but the specific type is unknown.  HIGH severity when not logged;
MEDIUM when logged but missing `exc_info=True`.  Acceptable only at top-level handlers
(worker loops, HTTP handlers) when properly logged with `exc_info=True`.  A handler that
unconditionally re-raises while preserving the trace is exempt — bare `raise`, `raise
X(...)`, or `raise X(...) from e` — because nothing is swallowed; `raise X(...) from
None` (which discards the trace) and a conditional re-raise that can fall through still
fire.

Exempt: handlers whose log call formats the exception through a recognised redaction
helper (redact*/sanitiz*/safe_traceback/…) — the failure is logged at a deliberate
no-traceback boundary.

---

## E005 — `ExceptBlockMissingExcInfo` {#e005}

**Tier:** `warn` · **Scope:** `both` · **Category:** `missing-traceback` · **Autofixable:** yes · **Since:** 0.2.0

> except block logs without exc_info=True — stack trace discarded

**Rationale:** Tracebacks are the primary artifact of incident postmortems. A record that says
something failed but carries no stack trace forces engineers to reproduce the failure —
often impossible under production data volumes.

The message is logged but the stack trace is lost.  Add `exc_info=True` to every
`logger.warning()` / `logger.error()` call inside an except block.  `logger.exception()`
is exempt (it implies `exc_info=True`).

Exempt: calls whose arguments flow through a recognised redaction helper
(redact*/sanitiz*/safe_traceback/scrub_secret*/mask_secret*) — these mark a deliberate
no-traceback boundary where exc_info=True would serialize the raw exception past the
sanitizer and can leak credentials (JDBC URLs, Authorization headers, OAuth bodies).

---

## E006 — `BareExceptWithBody` {#e006}

**Tier:** `block` · **Scope:** `both` · **Category:** `silent-swallow` · **Autofixable:** — · **Since:** 0.2.0

> Bare 'except:' (no type) — catches SystemExit and KeyboardInterrupt

**Rationale:** A bare except: absorbs KeyboardInterrupt and SystemExit even with a handler body.
Process-termination signals are silently intercepted and the process may continue in an
undefined state.

Like P001 but the block may have a body.  Still catches KeyboardInterrupt and
SystemExit.  Always specify at least `except Exception:`.

---

## E007 — `ErrorToReturnValue` {#e007}

**Tier:** `warn` · **Scope:** `both` · **Category:** `error-to-return-value` · **Autofixable:** — · **Since:** 0.2.0

> except block returns a value without logging — error hidden

**Rationale:** Converting an exception to a falsy return value (None, [], False) shifts the failure
point: the caller sees a plausible empty result and fails later, often somewhere
unrelated, making the original failure invisible.

Exception is converted to a return value (None, {}, [], False) with no trace.  Callers
see a wrong result with no idea why.  At minimum log before returning; prefer raising a
domain-specific exception instead.

---

## E008 — `ImportErrorWithoutLogging` {#e008}

**Tier:** `warn` · **Scope:** `both` · **Category:** `optional-import` · **Autofixable:** — · **Since:** 0.2.0

> except ImportError without logging — environment issues hidden

**Rationale:** Silent optional-import guards mask environment misconfigurations. If a preferred module
is unexpectedly absent, the fallback runs and produces subtly wrong results with no
signal in the observability stack.

Optional-dependency guard.  Acceptable when the import is genuinely optional AND the
fallback path is correct AND there is a comment.  Log at DEBUG if the module is
preferred but not required.  Flag if the module is expected to be present (will fail
later with a confusing AttributeError).

---

## E009 — `ExceptBlockOnlyAssigns` {#e009}

**Tier:** `warn` · **Scope:** `both` · **Category:** `error-to-return-value` · **Autofixable:** — · **Since:** 0.2.0

> except block only assigns a variable — error hidden with no log

**Rationale:** The exception sets a flag or default and the event record is destroyed. Callers see
apparently-normal behaviour until they discover the silent fallback later — typically
under production conditions where it produces wrong results at scale.

Exception sets a flag or default value with no trace.  Combines P007's error-hiding with
no logging.  Add a `logger.warning(..., exc_info=True)` before the assignment.

---

## E010 — `AsyncioGatherExceptionsUnexamined` {#e010}

**Tier:** `warn` · **Scope:** `both` · **Category:** `asyncio-unexamined` · **Autofixable:** — · **Since:** 0.2.0

> asyncio.gather(return_exceptions=True) results not checked for exceptions

**Rationale:** gather(return_exceptions=True) turns exceptions into values, indistinguishable from
normal results in the returned list. Every failed sub-task silently disappears unless
the caller checks each result for Exception.

`return_exceptions=True` returns exception instances as values in the result list.  If
the list is not subsequently inspected for `Exception` instances, errors vanish
silently.  The pattern is only a bug when results are not checked;
`return_exceptions=True` itself is acceptable.

---

## E011 — `LoggingFilterUnsafeBody` {#e011}

**Tier:** `warn` · **Scope:** `both` · **Category:** `filter-safety` · **Autofixable:** — · **Since:** 0.2.0

> logging.Filter.filter() body not wrapped in try/except — can crash caller

**Rationale:** logging.Filter.filter() exceptions propagate to the code that called logger.info() — not
to handleError() like handler exceptions. An unguarded filter body is a production crash
vector hidden inside observability infra.

`Logger.handle()` calls `self.filter(record)` with no surrounding try/except — unlike
handler errors, filter exceptions are NOT caught by `handleError()`.  An unguarded
attribute access (`record.custom_field`) or any external call that can raise will
propagate directly to the code that called `logger.info()` (or similar), crashing the
caller.  Wrap the entire `filter()` body in try/except with a safe fallback (return True
to pass-through, False to drop).  Use `getattr(record, 'field', default)` for optional
record attributes. Never acceptable — filter methods must never let exceptions
propagate.

---

## E012 — `UntypedBuiltinRaise` {#e012}

**Tier:** `warn` · **Scope:** `both` · **Category:** `untyped-raise` · **Autofixable:** — · **Since:** 0.2.0

> raise ValueError/RuntimeError/... where a typed AppError applies

**Rationale:** The Automation Engine receives a typed error envelope (category, audience, retryable,
code). A bare ValueError delivers an opaque string with none of these — dashboards are
blind, on-call routing can't branch on it, SLA gates can't classify it. (per ADR-0013)

SDK code raises a bare Python builtin.  The Automation Engine receives an opaque string
— no category, code, audience, or retryable field. Dashboards are blind; on-call routing
is impossible.  Use the typed error leaf from `application_sdk.errors`.  Acceptable only
inside dataclass `__post_init__` / stdlib validator methods where Python semantics
require `TypeError`/`ValueError` for stdlib interoperability.

---

## E013 — `LegacyAtlanErrorRaise` {#e013}

**Tier:** `block` · **Scope:** `both` · **Category:** `legacy-raise` · **Autofixable:** — · **Since:** 0.2.0

> raise ClientError/ApiError/... (deprecated AtlanError stack)

**Rationale:** AtlanError subclasses produce no typed wire envelope — they reach AE as opaque strings
and emit DeprecationWarning at construction. Every new raise site deepens the migration
debt and blocks the v4.0 removal.

`AtlanError` and its subclasses emit a `DeprecationWarning` at construction time and
reach AE as opaque strings.  They produce no typed wire envelope.  Scheduled for removal
in v4.0.  Replace with the appropriate leaf from `application_sdk.errors`.

---

## E014 — `ExceptLoopControlSwallow` {#e014}

**Tier:** `warn` · **Scope:** `both` · **Category:** `silent-swallow` · **Autofixable:** — · **Since:** 0.2.0

> except block exits loop silently (continue/break) without logging

**Rationale:** A loop that silently absorbs per-item exceptions can complete with a full-looking result
set that silently omits items. Silent partial failure is harder to detect than an
outright crash — callers may act on the wrong result for a long time.

An `except` block inside a loop whose body is only `continue`, `break`, or `pass` — with
no logging call — silently swallows the exception and resumes or exits the iteration.
This is the loop-body twin of P001/P002 (ruff S112 family).  At minimum log at DEBUG
before the loop control statement; if the exception signals a genuine error, re-raise or
log at WARNING/ERROR with `exc_info=True`.

---

## E015 — `ExceptionTextInErrorMessage` {#e015}

**Tier:** `warn` · **Scope:** `both` · **Category:** `error-message-hygiene` · **Autofixable:** — · **Since:** 0.2.0

> Caught exception text interpolated into typed error message= — leaks unsanitised text

**Rationale:** Embedding str(exc) in message= produces a unique string per failure instance — each
path/value becomes a separate dashboard bucket instead of one countable signal. It also
leaks unsanitised upstream text into a field shown to operators and indexed by
aggregation.

A typed `AppError` raise whose `message=` keyword value embeds the caught exception via
an f-string (`f'…{exc}…'`), `str(exc)`, `repr(exc)`, or string concatenation (`'…: ' +
str(exc)`) — see typed-error-prescription.md §6.  This leaks unsanitised, potentially
user-facing text from an upstream library into a field that is displayed to operators
and indexed in dashboards.  It also breaks aggregation by collapsing distinct failure
modes into one variable-text bucket.  Place the exception context in a typed evidence
field (`cause=exc`, `network_error=str(exc)`) and keep `message=` a stable human
summary.

---

## E016 — `MissingExceptionChaining` {#e016}

**Tier:** `warn` · **Scope:** `both` · **Category:** `exception-chaining` · **Autofixable:** yes · **Since:** 0.2.0

> raise inside except block missing 'from exc' cause — breaks exception chain

**Rationale:** When the Automation Engine rebuilds the typed wire envelope it walks the
.cause/__cause__ chain (set by 'raise X from e'); its reconstruction path does not
follow __context__. Without explicit chaining the original exception — attached only as
__context__ — is visible at the interpreter level but dropped from the wire envelope and
every downstream system that reads it.

A non-bare `raise` inside an `except … as e:` block that does not include `from e` (or
`from None`).  Without explicit chaining, Python attaches the original as `__context__`
with `__suppress_context__=False` — the chain is preserved at the interpreter level but
AE's wire serialiser only follows the `__cause__` chain, so the original exception is
lost in dashboards.  Fix: `raise NewError(...) from e`.  Use `from None` only when
intentionally hiding the original (requires a comment explaining why). Acceptable
patterns: bare `raise` (re-raise), `raise X() from e` (already chained), `raise X() from
None` (intentional suppression).

---

## E017 — `SecretNamedEvidenceKey` {#e017}

**Tier:** `block` · **Scope:** `both` · **Category:** `security` · **Autofixable:** — · **Since:** 0.2.0

> Error evidence kwarg ending in _secret/_password/_token — rejected by wire layer at runtime

**Rationale:** Evidence fields with secret-bearing suffixes are rejected by the wire layer at runtime.
Static detection catches the pattern before any code runs, eliminating the window
between deploy and first invocation where live credentials could be serialised into
logs, dashboards, or SARIF.

An error construction call that passes a keyword argument whose name ends in `_secret`,
`_password`, or `_token` — see `application_sdk.errors.wire` §6.  The wire layer
actively rejects these suffixes at runtime (`ValueError`) to prevent credential leakage
into logs, dashboards, and SARIF reports.  Static detection means the bug is caught
before any code runs.  Rename the evidence field to a safe key (e.g. `credential_name`,
`token_type`) or pass the value via `cause=exc`.

---

## E018 — `BareParentLeafRaise` {#e018}

**Tier:** `warn` · **Scope:** `both` · **Category:** `untyped-raise` · **Autofixable:** — · **Since:** 0.2.0

> Raising a bare AppError leaf class without a domain subclass overriding code

**Rationale:** Each categorical leaf is a dashboard bucket. Raising the parent directly collapses all
domain failure modes into one bucket — impossible to count separately, route to the
right rotation, or alert on per failure mode.

Raising a parent leaf directly (`InternalError(...)`, `InvalidInputError(...)`) without
a domain-specific subclass that overrides `code` collapses all failure modes for a given
category into one dashboard bucket — impossible to route, alert on, or triage
individually.  See typed-error-prescription.md §4.  Only sanctioned bare-parent form:
`InternalError(classification_pending=True)` — used exclusively as a temporary
placeholder during migration, never in production-stable code.  For all other sites:
define a domain subclass that overrides `code` with a specific constant (e.g.
`ENGINE_NOT_INITIALIZED`). Note: this rule is WARN tier because some bare-parent raises
may be legitimate in small apps or during active migration.  Review findings before
suppressing.

---

## E019 — `ExceptionTextInContractField` {#e019}

**Tier:** `warn` · **Scope:** `both` · **Category:** `error-message-hygiene` · **Autofixable:** — · **Since:** 0.9.0

> Caught exception text interpolated into a returned contract message= field — leaks unsanitised text

**Rationale:** E015 stops exception text leaking through a raised AppError's message=, but the same
text leaks just as readily when an except block returns a typed response contract
(AuthOutput, PreflightCheck) with message=str(exc). The returned value crosses the typed
boundary to operators and dashboards, and each distinct str(exc) becomes its own
aggregation bucket instead of one countable signal.

Inside an `except … as exc:` block, a call (typically a typed response/output contract
such as `AuthOutput` or `PreflightCheck`) is constructed with a `message=` keyword that
embeds the caught exception — whether that call is returned (`return
AuthOutput(message=str(exc))`), appended/collected for a later return
(`checks.append(PreflightCheck(message=f'…{exc}'))`), or simply assigned.  The
interpolation may be an f-string (`f'…{exc}…'`), `str(exc)`, `repr(exc)`, or string
concatenation (`'…: ' + str(exc)`). This is the non-`raise` counterpart of E015: the
unsanitised upstream text still crosses the typed boundary into a field shown to
operators and indexed in dashboards, and still collapses distinct failure modes into one
variable-text bucket.  Keep `message=` a stable human summary and carry the exception
detail in a typed field (e.g. raise a typed `AppError` with `cause=exc` upstream, or
record it in a dedicated evidence field) rather than the user-facing contract message.

Detection scope mirrors E015 exactly (they share one matcher): it covers f-string,
`str(exc)`, `repr(exc)` and string-concatenation (`'…' + str(exc)`) interpolation of the
except binding, but not a bare `message=exc` or attribute access such as
`message=exc.args[0]`. Extending both rules to those shapes is a deliberate future
follow-up kept symmetric across E015/E019.

---

## E020 — `HttpFailureToEmptyReturn` {#e020}

**Tier:** `warn` · **Scope:** `app` · **Category:** `error-to-return-value` · **Autofixable:** — · **Since:** 0.9.0

> Checked HTTP-response failure returns an empty/None sentinel instead of raising — silently publishes a failure as success

**Rationale:** A guard that checks an HTTP response for failure and then returns an empty/None sentinel
converts a remote API failure into an empty successful result — the workflow publishes a
zero or partial crawl as if it completed, with no error surfaced. Unlike E001/E002/E007
there is no except/raise to key on; the failure is swallowed by a plain if-guard.

An `if` whose test inspects an HTTP response for failure (a negation or comparison on
`is_success` / `ok` / `status_code`) and whose branch `return`\ s an empty/None sentinel
(`return`, `None`, `[]`, `{}`, `()`, `""`, `list()`/`dict()`/`set()`).  The remote
failure is coerced into an empty *successful* result, so the workflow reports a
zero/partial crawl as complete and no error reaches the operator or the Automation
Engine.

Both polarities are covered: failure-in-the-test → empty body (`if not resp.is_success:
return []`) and its mirror, success-in-the- test → empty `else` (`if resp.is_success: …
else: return []`).

This escapes the rest of the E-series because there is no `except`/`raise` — it is a
plain response guard.  Fix: raise a typed `AppError` (e.g. `DependencyUnavailableError`)
on the failure branch so it propagates.  Anchored on HTTP-response markers and a
failure-shaped test to avoid flagging ordinary `if x is None: return None` guards;
suppress with `# conformance: ignore[E020] <reason>` where an empty result is the
deliberate, documented contract.

---
