◆ NFRGate / Rule Reference

L1 — No swallowed exceptions

high 📝 logs · static analyzable

Every catch/except block on a request-handling or external-call path emits a log entry. Fail if an empty or pass-through handler exists on those paths.

Python Implementation

L1: every catch/except block on a request-handling or external-call path
emits a log entry; fail if an empty or pass-through handler exists.
rubric_store/definitions/logs.yaml tags this fully static_analyzable.

Distinct from L2 (structured_log_rule.py) even though both inspect catch
blocks: L1 asks "was this exception swallowed silently" (empty body, bare
`pass`, or a no-op re-raise with nothing else) — L2 assumes a log call
exists and asks whether *that call* is structured. A catch block that logs
something can still fail L2 (unstructured message) while passing L1, and
vice versa isn't possible by construction (L1 requires a log call to pass,
so a block with a log call never trips L1's empty/pass-through check) —
they're deliberately not merged into one rule because they answer different
rubric questions with different evidence.

This rule is scoped to what "empty or pass-through" can mean from syntax
alone; a catch block that does something else entirely (sets a flag, calls
a non-logging function) without logging is a real ambiguous case the
rubric's literal wording doesn't clearly call a fail on its own — see the
"unclear" branch below rather than a guessed fail.

Fail-branch confidence retuned from 0.9 to 0.714, per
docs/false_positive_rate_study_v1.md: a small hand-labeled sample (n=3,
pooled with Go's "returns without including the error" fail branch —
Java's L1 wasn't sampled at all) scored 3/3 correct. Pass/unclear are
untouched (not sampled).

Java Implementation

L1 for Java: every catch block on a request-handling or external-call
path emits a log entry; fail if an empty or pass-through handler exists.
Same "empty or bare re-throw" scoping as the Python version
(no_swallowed_exception_rule.py) — Java has no `pass` keyword, so an empty
block (`catch (Exception e) {}`) is the direct equivalent, and a bare
`throw e;` (the *same* exception, verbatim — see is_empty_or_pass_through's
own docstring on why that distinction matters) with nothing before it is
the re-throw equivalent.

Fail-branch confidence pooled from 0.9 to 0.714 with the existing
Python/Go value (docs/confidence_retuning_v1.md), per
docs/product_readiness_report_v1.md's follow-up pass: this rule's own
is_empty_or_pass_through was over-matching "throw wrap(e);" as a bare
re-throw until fixed there, which is the *same* root cause the original
cross-language pooling was already built on (a single-statement re-throw
with nothing else, and no log) — now that the detection logic actually
implements that definition correctly here too, it inherits the same
validated confidence rather than staying at its old, never-independently-
validated 0.9.

Go Implementation

L1 for Go: an `if err != nil { ... }` block on a request-handling or
external-call path emits a log entry; fail if the error is discarded.

Not a straight port — Go's idiomatic "log at the boundary, propagate
elsewhere" convention means "no log call here" isn't automatically a fail
the way an empty Python/Java catch block is. This rule distinguishes three
real Go shapes:

- The block is empty, or returns without including the error at all
  (`if err != nil { return }` when the function could return it) — the
  error is genuinely discarded, the caller has no way to know it happened.
  That's an unambiguous fail.
- The block returns the error (`return err`, `return nil, err`,
  `return fmt.Errorf("...: %w", err)`) with no log call here — legitimate
  under Go's log-at-the-boundary convention, so this is "unclear" rather
  than a guessed fail, matching StructuredLogRuleGo's L2 reasoning for the
  identical ambiguity.
- A log call is present anywhere in the block — pass, regardless of the
  above, since the requirement (an entry gets logged) is satisfied.

The "returns without including the error" fail branch's confidence is
retuned from 0.65 to 0.714, per docs/false_positive_rate_study_v1.md — a
2-sample hand-labeled check on real code, pooled with the Python
implementation's equivalent claim (n=3 total; see
no_swallowed_exception_rule.py). The "empty block" fail branch (0.8) is
untouched — that shape wasn't sampled at all, and it's arguably an even
clearer swallow than what was tested, so there's no basis to move it either
direction.
← All rules