Skip to content

eslint-factory: require-fs-sync-try-catch treats a catch-less try/finally as protective (soundness gap in isInsideTryBlock) #44219

Description

@github-actions

Rule

require-fs-sync-try-catch.

Summary

isInsideTryBlock (require-fs-sync-try-catch.ts:33-55) considers a call "protected" whenever it is lexically inside the block of any enclosing TryStatement:

if (ancestor.type === "TryStatement" && !crossedDeferredBoundary) {
  const block = ancestor.block;
  if (node.range ... within block.range) {
    return true;   // <- treated as protected
  }
}

It never checks whether that TryStatement actually has a catch handler. A try { ... } finally { ... } with no catch does not swallow the error — the throw propagates past finally and still crashes the action. Yet the rule marks the fs call inside such a try as safe and does not flag it.

This defeats the rule's own stated contract ("an unhandled throw crashes the action"): a catch-less try/finally provides cleanup, not error handling, so the crash the rule exists to prevent still happens.

Failure scenario

function persist(path, data) {
  acquireLock();
  try {
    fs.writeFileSync(path, data);   // throws on ENOSPC / EACCES
  } finally {
    releaseLock();                  // runs, then the throw resumes propagating
  }
}

The rule sees the enclosing TryStatement and stays silent, but persist re-throws and the action dies with the same unhandled fs error the rule is meant to catch. try/catch (or try/catch/finally) would genuinely handle it; try/finally alone does not.

Grounding

Ungrounded in the current corpus: a scan for try { ... fs.<sync> ... } finally over non-test actions/setup/js/**/*.cjs returned 0 live sites today (the codebase consistently uses try { fs... } catch). This is a latent soundness gap in the protection predicate rather than a currently-firing miss — filing it because it is a small, well-contained logic fix that closes a real hole before the idiom appears, and because the same catch-less-try assumption may be worth auditing in sibling try/catch rules (require-json-parse-try-catch, require-fs-sync-style guards).

Ask

  1. In isInsideTryBlock, only treat a TryStatement as protective when it has a catch handler — i.e. require ancestor.handler != null before returning true. (A try/catch/finally still has handler, so it stays protective; a finally-only try does not.)
  2. Add tests:
    • invalid: try { fs.writeFileSync(p, d); } finally { cleanup(); } — should be flagged.
    • valid: try { fs.writeFileSync(p, d); } catch (e) {} finally { cleanup(); } — should stay unflagged.
  3. Confirm the existing valid cases (plain try { fs... } catch (e) {}) are unaffected.

Notes

Statically grounded review (no live lint available: eslint-factory has no node_modules/dist here, npm firewalled). Verified against require-fs-sync-try-catch.ts + try-catch-rule-utils.ts and the live *.cjs corpus.

Generated by 🤖 ESLint Refiner · 307.7 AIC · ⌖ 12.6 AIC · ⊞ 4.6K ·

  • expires on Jul 14, 2026, 10:19 PM UTC-08:00

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions