Skip to content

no-unsafe-catch-error-property: rename alias of catch param bypasses unsafe-property detection #65255

Description

@github-actions

Summary

no-unsafe-catch-error-property only tracks unsafe .message/.stack/.code/.status/.cause/.name access on the catch clause's own parameter identifier (top.varName, set once in the CatchClause visitor). It never follows a simple rename alias (const err = error;) created inside the catch block, so any unsafe property access performed through the alias escapes detection entirely.

Its sibling rule, no-caught-error-interpolation, already solves this for bare-identifier interpolation: isCaughtErrorIdentifier (eslint-factory/src/rules/no-caught-error-interpolation.ts:145-160) explicitly resolves one hop through a const alias = caughtVar; declarator back to the original catch/rejection-handler binding before deciding whether an identifier is a caught error. no-unsafe-catch-error-property has no equivalent resolution step — its MemberExpression visitor (eslint-factory/src/rules/no-unsafe-catch-error-property.ts:268-289) compares obj.name directly against the catch param's own name and nothing else.

Live grounding (3 sites, same PR, 2 files)

All three are new/changed in actions/setup/js/codex_harness.cjs (part of today's Codex engine fix):

// codex_harness.cjs:327-332
} catch (error) {
  const err = /** @type {Error} */ error;
  // ...
  throw new Error(`--prompt-file '${promptFile}' is not readable: ${err.message}`, { cause: err });
}
// codex_harness.cjs:570-574
} catch (error) {
  const err = /** @type {Error} */ error;
  log(`warning: unable to configure provider endpoint from /reflect: ${err.message}`);
  return { env, configured: false };
}
// codex_harness.cjs:732-736
} catch (err) {
  const e = /** @type {Error} */ err;
  log(`fatal: ${e.message}`);
  process.exit(1);
}

In every case the catch binding (error/err) is renamed via a const declarator carrying only a JSDoc @type cast comment (no runtime narrowing — error/err could still be a non-Error throw), then .message is read off the alias, not the original binding. The rule's MemberExpression visitor never sees these because obj.name is err/e, not the tracked varName (error/err).

Confirmed via the rule's own test file (no-unsafe-catch-error-property.test.ts:99) that a fresh const e = new Error() is correctly treated as valid (it isn't derived from the catch binding) — but there is no test for a const e = <catchParam>; rename, which is the actual live pattern above.

Suggested fix

Port the one-hop alias resolution already used by no-caught-error-interpolation (isCaughtErrorIdentifier, lines 145-160): when a CatchClause is entered, additionally register any const <alias> = <catchParam>; declarator found in the same block as a second tracked name (or resolve MemberExpression.object through the scope chain back to the catch param) before deciding whether .message/.stack/etc. access is unsafe.

Acceptance criteria

  • catch (error) { const err = error; use(err.message); } (no guard) is flagged, with err reported as the unsafe variable.
  • The existing valid case catch (err) { const e = new Error(); console.log(e.message); } remains valid (fresh Error, not an alias of the catch binding, must not regress).
  • Guarded aliases (const err = error; if (err instanceof Error) { err.message }) remain valid — alias resolution must feed into the existing guard logic (isGuardedByAncestorBranch / isCallOrderingGuarded / hasPriorEarlyExitInstanceofGuard), not just suppress/report blindly.
  • New test cases added to no-unsafe-catch-error-property.test.ts covering the alias-rename shape (both flagged and guarded).

Generated by 🤖 ESLint Refiner · claude · agent · 364.6 AIC · ⌖ 6.94 AIC · ⊞ 5.2K · ◷

  • expires on Oct 9, 2026, 9:40 PM UTC-08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions