Skip to content

feat(prefer-throw-if-no-entry): add rule - #139

Merged
43081j merged 2 commits into
e18e:mainfrom
webpro:rule-prefer-throw-if-no-entry
Jul 3, 2026
Merged

feat(prefer-throw-if-no-entry): add rule#139
43081j merged 2 commits into
e18e:mainfrom
webpro:rule-prefer-throw-if-no-entry

Conversation

@webpro

@webpro webpro commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add opt-in prefer-throw-if-no-entry
  • report statSync/lstatSync calls inside try/catch when they do not pass { throwIfNoEntry: false }
  • provide suggestions to add { throwIfNoEntry: false } where the stat options object can be safely edited

Notes

This is suggestion-only and not part of the recommended config. It skips try/finally without catch, nested functions, calls that already pass { throwIfNoEntry: false }, and fstatSync because file-descriptor stat calls do not have a missing path entry case.

Node v24.9.0 benchmark for 200,000 missing statSync calls:

  • try/catch miss path: 679.6ms
  • { throwIfNoEntry: false } miss path: 147.3ms

Test plan

  • npm test -- src/rules/prefer-throw-if-no-entry.test.ts
  • npm run lint
  • npm run build

@webpro
webpro force-pushed the rule-prefer-throw-if-no-entry branch from 31838cb to b443174 Compare June 27, 2026 12:43
@43081j

43081j commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

can you explain this one a bit more?

what's the benefit of try/catching it with throwIfNoEntry: false? to ensure the only thrown errors are fatal ones?

@webpro

webpro commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

can you explain this one a bit more?

It's a performance rule, to promote { throwIfNoEntry: false }. This (only) suppresses the ENOENT case and saves the expensive Error obj + stack trace. It's a bit of a niche rule perhaps, but when probing lots of files/dirs might save significant time (benchmarked) and memory (stack traces are large objects?).

what's the benefit of try/catching it with throwIfNoEntry: false? to ensure the only thrown errors are fatal ones?

Exactly. Real errors such as EACCES still throw.

@43081j

43081j commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

It's a performance rule, to promote { throwIfNoEntry: false }.

this is the "how", but what is the "why"?

try { statSync(p, {throwIfNoEntry: false}); } catch {}

this is one of the fixed outputs, but in what situation would someone do this? wouldn't the reason to set it to false be to remove the try/catch?

@webpro

webpro commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

It only "suppresses" ENOENT (the common case), we still need to catch EACCES and other errors.

Edit: btw statSync returns undefined if file does not exist.

@43081j

43081j commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

i think i'm missing some context still though.

yes this is a rule to promote throwIfNoEntry: false. But why?

statSync does indeed return undefined.

stat and promises.stat both use this flag to decide if to return undefined when not found, or if to throw.

we both fully understand how they work. i'm not asking that, i'm asking why someone would migrate from throwing to not throwing.

@webpro

webpro commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Performance: statSync is a lot faster (and presumably memory-efficient) if it does not throw in case the file does not exist. If the logic is probing many (potentially missing) files this adds up. Did a quick benchmark which is in the description.

Granted, might be a bit of a niche rule but a shame to overlook in certain fs-heavy situations.

References:

@webpro
webpro force-pushed the rule-prefer-throw-if-no-entry branch from b443174 to c1b66a6 Compare June 28, 2026 18:52
@43081j

43081j commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

aha i understand my source of confusion now.

given this as a "before":

try {
  fs.statSync(...);
} catch {
  // if ENOENT do something because it wasn't found
  // otherwise, rethrow
}

the "after" is actually this:

const stat = fs.statSync(..., { throwIfNoEntry: false });
if (!stat) {
  // do something because it wasn't found
}

i.e. the real benefit is when you're using a try/catch to determine if a file exists or not. you can now do that faster with this option and just compare the result against undefined.

the example in the test cases isn't this. it would gain still because it is unconditionally throwing away all errors anyway, so never throwing the not-found one will speed it up. but it doesn't seem like code anyone would write (doing a stat where you don't check the result OR the error, is just no-op).

maybe its worth making this clearer somehow in the description. the message already touches on it

The message and description now state that the try/catch stays for real
errors (EACCES, ...); only the common not-found case moves out of the
catch into a return-value check. Replace the no-op empty-catch example
with a realistic isDir probe so the test cases show the intended pattern.
@webpro

webpro commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Added an example and tried to clarify in docs.

Regardless of nested try/catch blocks, I think we should always catch such known errors. Another way to look at it:

Before:

try {
  use(statSync(p));
} catch (err) {
  if (err.code === 'ENOENT') return; // missing
  logger.warn(err);                  // real error
}

After:

try {
  const stat = statSync(p, { throwIfNoEntry: false });
  if (!stat) return;                 // missing, no throw
  use(stat);
} catch (err) {
  logger.warn(err);                  // real error, still caught
}

it doesn't seem like code anyone would write (doing a stat where you don't check the result OR the error, is just no-op).

Maybe the before/after example above is more clear. It's what Node.js and TS internally do because it's faster.

@43081j

43081j commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

new messaging looks much better! seems a lot clearer now.

thanks a bunch for helping me better understand 🙏

@43081j
43081j merged commit 0317f98 into e18e:main Jul 3, 2026
3 checks passed
@webpro

webpro commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Should've clarified intent/reasoning better in the description right away. Anyway, thanks for merging! Like how this plugin is evolving 😇

@webpro
webpro deleted the rule-prefer-throw-if-no-entry branch July 5, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants