Summary
require-error-cause-in-rethrow (the newly added 11th rule) tracks only the catch-clause parameter identifier as the "caught variable." It does not follow the extremely common normalization idiom where the catch binding is immediately aliased into a local, TS-friendly variable:
catch (deleteError) {
/** `@type` {any} */
const err = deleteError; // <-- normalization alias
...
throw new Error(`... ${message || String(err)}`); // references `err`, NOT `deleteError`
}
Because expressionReferencesCatchVar searches for the catch param name (deleteError) and the rethrow message only references the alias (err), the rule never fires — the original error chain is silently dropped and the rule's whole reason for existing is defeated.
Grounding
The /** @type {any} */ const err = <catchError>; idiom is used in 5 corpus files:
| file |
catch param |
alias |
actions/setup/js/create_pull_request.cjs:573-575 |
deleteError |
const err = deleteError |
actions/setup/js/push_signed_commits.cjs:671-673 |
createRefError |
const err = createRefError |
actions/setup/js/dispatch_workflow.cjs:272-274 |
dispatchError |
const err = dispatchError |
actions/setup/js/route_slash_command.cjs:341 |
dispatchError |
const err = dispatchError |
actions/setup/js/mcp_server_core.cjs:830 |
error |
const err = error |
Live true-negative (rule should fire, doesn't): actions/setup/js/create_pull_request.cjs:589:
} catch (deleteError) {
const err = deleteError;
...
throw new Error(`Failed to delete existing remote branch "${branchName}" for reuse with recreate-ref: ${message || String(err)}`);
}
This rethrows a brand-new Error derived from the caught value, with no { cause } — exactly the defect the rule targets — yet it escapes detection purely because of the alias.
This is a growing blindspot: as more catch blocks adopt the @type {any} normalization idiom for TypeScript-checked .cjs, every rethrow inside them becomes invisible to the rule.
Acceptance criteria
- When a catch body contains
const <alias> = <catchParam>; (a simple, un-reassigned local initialized directly from the catch binding), treat references to <alias> in the rethrown new Error(...) as references to the caught variable.
- Scope the alias resolution to the catch body and require the initializer be exactly the catch param identifier (no method calls / member access) to stay conservative.
- The reported/suggested
{ cause: ... } should use whichever identifier is in scope and correct (prefer the alias if the catch param is shadowed/renamed, else the catch param).
- Do not introduce false positives: a
const x = someUnrelatedValue; must not be treated as an alias, and a rethrow via bare throw err; (no new Error) must still be ignored.
- Add RuleTester coverage: (a) invalid — aliased catch var referenced in
new Error message without cause, mirroring create_pull_request.cjs:589; (b) valid — same but with { cause: err }; (c) valid — alias initialized from an unrelated value; (d) valid — alias reassigned before the throw (too complex to track → do not flag).
- Re-verify against create_pull_request.cjs:589 after the change: it should now be flagged (or the site fixed to add
{ cause }).
Notes
Related but distinct precision gap (filed separately): expressionReferencesCatchVar does not traverse LogicalExpression/ConditionalExpression, which independently hides the same ${message || String(err)} site. Both gaps must be closed for line 589 to be detected.
Generated by 🤖 ESLint Refiner · 197.1 AIC · ⌖ 13.3 AIC · ⊞ 4.6K · ◷
Summary
require-error-cause-in-rethrow(the newly added 11th rule) tracks only the catch-clause parameter identifier as the "caught variable." It does not follow the extremely common normalization idiom where the catch binding is immediately aliased into a local, TS-friendly variable:Because
expressionReferencesCatchVarsearches for the catch param name (deleteError) and the rethrow message only references the alias (err), the rule never fires — the original error chain is silently dropped and the rule's whole reason for existing is defeated.Grounding
The
/**@type{any} */ const err = <catchError>;idiom is used in 5 corpus files:actions/setup/js/create_pull_request.cjs:573-575deleteErrorconst err = deleteErroractions/setup/js/push_signed_commits.cjs:671-673createRefErrorconst err = createRefErroractions/setup/js/dispatch_workflow.cjs:272-274dispatchErrorconst err = dispatchErroractions/setup/js/route_slash_command.cjs:341dispatchErrorconst err = dispatchErroractions/setup/js/mcp_server_core.cjs:830errorconst err = errorLive true-negative (rule should fire, doesn't):
actions/setup/js/create_pull_request.cjs:589:This rethrows a brand-new
Errorderived from the caught value, with no{ cause }— exactly the defect the rule targets — yet it escapes detection purely because of the alias.This is a growing blindspot: as more catch blocks adopt the
@type {any}normalization idiom for TypeScript-checked.cjs, every rethrow inside them becomes invisible to the rule.Acceptance criteria
const <alias> = <catchParam>;(a simple, un-reassigned local initialized directly from the catch binding), treat references to<alias>in the rethrownnew Error(...)as references to the caught variable.{ cause: ... }should use whichever identifier is in scope and correct (prefer the alias if the catch param is shadowed/renamed, else the catch param).const x = someUnrelatedValue;must not be treated as an alias, and a rethrow via barethrow err;(nonew Error) must still be ignored.new Errormessage without cause, mirroring create_pull_request.cjs:589; (b) valid — same but with{ cause: err }; (c) valid — alias initialized from an unrelated value; (d) valid — alias reassigned before the throw (too complex to track → do not flag).{ cause }).Notes
Related but distinct precision gap (filed separately):
expressionReferencesCatchVardoes not traverseLogicalExpression/ConditionalExpression, which independently hides the same${message || String(err)}site. Both gaps must be closed for line 589 to be detected.