Skip to content

Enhancement: [prefer-promise-reject-errors] option to allow 'rethrow' of signal reasons or caught values #11095

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
4 tasks done
turbocrime opened this issue Apr 23, 2025 · 2 comments
Labels
enhancement: plugin rule option New rule option for an existing eslint-plugin rule package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look

Comments

@turbocrime
Copy link

turbocrime commented Apr 23, 2025

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Link to the rule's documentation

https://typescript-eslint.io/rules/prefer-promise-reject-errors/

Description

When performing more complex async operations, a developer may call the reject executor of a Promise to propagate an AbortSignal reason, a value obtained by a catch block, or the reason parameter of a .then(_, onrejected) or .catch() callback.

in this case, it's often desirable to reject with the exact reason, to accurately propagate failures without changing them.

but, prefer-promise-reject-errors may complain unless these values are consistently verified.

it makes sense that some users of this rule may want the rule to enforce checking, assertion, or wrapping of reasons here, but i think it's common and good practice in many cases to simply propagate reasons without modifying them.

the rule is also applied inconsistently, and a few examples are provided.

Fail

// the rule must be disabled D: this should be unnecessary
function rejectOnSignal(signal: AbortSignal) {
  return new Promise<never>((_, reject) => {
    // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rethrow
    signal.addEventListener('abort', () => reject(signal.reason));
  });
}
// the rule must be disabled D: this should be unnecessary
function rerejectOnCatch(param: Promise<unknown>) {
  return new Promise<never>((_, reject) => {
    // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rethrow
    param.catch(e => reject(e));
  });
}

Pass

// yay the rule allows rethrow :D it's not necessary to disable it
function rejectOnSignal(signal: AbortSignal) {
  return new Promise<never>((_, reject) => {
    signal.addEventListener('abort', () => reject(signal.reason));
  });
}
// yay the rule allows rethrow :D it's not necessary to disable it
function rerejectOnCatch(param: Promise<unknown>) {
  return new Promise<never>((_, reject) => {
    param.catch(e => reject(e));
  });
}

Additional Info

the signal example is surprisingly common - an event listener is the most certain and reliable way to immediately react to signal activation.

the promise catch/rethrow is even more contrived, and debatable, but it makes sense to me that it should be possible to propagate a 'caught' value with no intervention.

inconsistent application: things that are presently permitted, but seem to violate the rule

rethrow behavior may be achieved a couple other ways, which the rule doesn't complain about, but i think that's a problem (the rule is inconsistent) rather than an acceptable workaround.

for example, this is permitted without complaint, but it appears to mostly be a failure to understand the reject of Promise.withResolvers as a promise executor.

function rejectOnSignal(signal: AbortSignal) {
  const { promise, reject } = Promise.withResolvers<never>();
  signal.addEventListener('abort', () => reject(signal.reason));
  return promise;
}

this triggers no complaints. it's kind of strange and i would also consider failure to detect this a lower priority, like the handler assignment. notably, this technique was the only way to obtain promise executor callbacks outside of the promise executor scope, before Promise.withResolvers became available.

export function rejectOnSignal(signal: AbortSignal) {
  let rejectExecutor;
  const promise = new Promise<never>((_, reject) => {
    rejectExecutor = reject;
  });
  signal.addEventListener('abort', () => rejectExecutor(signal.reason));
  return promise;
}

this next example is incorrect for a few reasons (the signal itself becomes the propagated value, this may clobber existing handlers), this is permitted without complaint. this appears mostly to be a failure to understand that assigning reject to an event handler attribute will result in a call to reject.

function rejectOnSignal(signal: AbortSignal) {
  return new Promise<never>((_, reject) => {
    signal.onabort = reject;
  });
}
@turbocrime turbocrime added enhancement: plugin rule option New rule option for an existing eslint-plugin rule package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Apr 23, 2025
@kirkwaiblinger
Copy link
Member

Closely related: #10376, #10717

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Apr 28, 2025

Hi @turbocrime!

Reading more closely, I think there's several separately actionable things discussed here:

  1. Support Promise.withResolvers() in the rule.
  2. Support abortSignal in the rule
  3. Support somePromise.catch((e) => { Promise.reject(e); }); (and its corollaries)

Would you say that's accurate? If so, I'm thinking we constrain this issue to be about 3. and I'd invite you to file a new issue to discuss 1. WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement: plugin rule option New rule option for an existing eslint-plugin rule package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look
Projects
None yet
Development

No branches or pull requests

2 participants