Skip to content
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

Bug: [no-floating-promises] False positive for spread arguments in then() #8958

Closed
4 tasks done
jamonserrano opened this issue Apr 20, 2024 · 3 comments
Closed
4 tasks done
Labels
bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin working as intended Issues that are closed as they are working as intended

Comments

@jamonserrano
Copy link

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Playground Link

https://typescript-eslint.io/play#ts=5.4.3&fileType=.ts&code=C4TwDgpgBAShDOB7ANgN2gXigClQQ2QFcIAuKVRASwBMoAfKABQCdEBbS%2BCAGUoGsIAHgo0AfAEooGUeSrUA3AChQkWBABWEAMbApOZhDxIAdgH4yhY32OIA7scnTZNJYq2Jj8XQG8oBpGgQADR%2BGtq6AL5k3v4o6GRwAeghBpo6CWE6EXos7JwQAHS2lMAAFolxEMzwwnKi2OJK7p66sYHVZADaFYEhcGnAALp6nW3JoQODrgD001AAYgRcUGCI8CWU6FC2pRDGoUlV8FB4BlDwYAZ4tJTGwIhQZXsNirkcXAVjEA0FT8bYBUBX2qjUUilmUAAchAAOZ4YCbaCvVjvQpfH5-bDA%2BCdAAMgxSCEq1U6AEZBqCgA&eslintrc=N4KABGBEBOCuA2BTAzpAXGYkACAXAngA4oDG0AlobgLQrzkB2uA9AwPbUBm8bAhrowDm1QtDYBbcshTooiaGOiQAviGVA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false

Repro Code

const { resolve, reject } = Promise.withResolvers<void>();
const resolvers = [resolve, reject];

Promise.resolve().then(...resolvers);

ESLint Config

module.exports = {
  parser: "@typescript-eslint/parser",
  rules: {
    "@typescript-eslint/no-floating-promises": "error",
  },
};

tsconfig

{
  "compilerOptions": {
    // ...
  }
}

Expected Result

I expect no errors on line 4 when spreading resolvers into then().

Actual Result

There's an error on line 4:

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.

Additional Info

No response

@jamonserrano jamonserrano added bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for maintainers to take a look labels Apr 20, 2024
@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Apr 20, 2024

Thanks for the report @jamonserrano!

This is an interesting one. I definitely see what you're saying, but I'm uncertain there's a great solution.

In particular, with the exmple you've given, resolvers will have type Array<(value: void | PromiseLike<void>) => void>. You and I know that that array has 2 elements, because we can see where it's been constructed, but we have no way to know this in general.

const resolvers: Array<(value: void | PromiseLike<void>) => void> = [];

const { resolve, reject } = Promise.withResolvers<void>();
if (condition) {
   resolvers.push(resolve, reject);
} else if (anotherCondition) {
   resolvers.push(resolve);
} else {
   // do nothing
}

// resolvers might have 0, 1, or 2 elements.
Promise.reject().then(...resolvers);

Tuple types

In the playground link, you've done things a bit differently, so that resolvers is a tuple rather than an array. Tuples do have fixed length, so I agree with you that we could handle that case.

declare const resolvers: [() => void, () => void];
// we have enough information here with the types to be able to say that a catch callback is provided.
Promise.reject().then(...resolvers);

We have done something like this before use-unknown-in-catch-callback-variables, though it's never going to handle all cases perfectly, since you can have unions of tuple and things like that. To be honest, it's a fun programming puzzle, but maybe of questionable value 😆.

Please let us know if you have a natural use case where this would be handy, though! My thought is that you kind of have to go out of your way to create a tuple in the first place, either by as const or explicitly const resolvers: [Resolve, Reject] = ..., so I'm unclear how this might occur organically.

Even if we tightened up the checks around tuples, this wouldn't address the code you've provided in the issue report.

What I would do if I were you

At first glance, I would probably recommend anyways refactoring your code to pass the 2 explicit arguments. That makes it easy for a human to read and for our rule to inspect correctly 🙂. Do you have a concrete use case where that is unwieldy?

const [resolve, reject] = resolvers;
Promise.reject().then(resolve, reject);

Bugs

The rule does have at least one related bug, though, in that it doesn't currently distinguish between normal and rest args at all. So you can trick it with

const emptyResolvers: Array<() => void> = [];
// error, correct
Promise.reject().then(...emptyResolvers);
// no error, bug! The rule is tricked by there being two arguments,
Promise.reject().then(...emptyResolvers, ...emptyResolvers);

That much we should definitely fix.


Would love to hear your thoughts on the above if you have time to read through it!

@kirkwaiblinger kirkwaiblinger added awaiting response Issues waiting for a reply from the OP or another party and removed triage Waiting for maintainers to take a look labels Apr 20, 2024
@bradzacher
Copy link
Member

I would tend to agree.
I think this is something that's okay sitting unhandled given this complexity and how rare it is - most people will not create a new array to house two parameters to spread it and would instead just pass the two parameters directly.

I struggle to think of real world code where you would actively chose to write code like this, TBH. The array form seems very, very opaque as an API design choice and only really supports void-returning promises. It also doesn't let you handle errors...

@jamonserrano
Copy link
Author

@kirkwaiblinger Thanks for the thorough response!

In our application we use events to bridge the gap between React and the communication layer, and this solution intended to improve the developer experience of handling requests. The resolvers were not extracted from actual promises, they were just there to provide a familiar API. In the end I just delegated these events to a wrapper function that handles the promise, so the resolvers are now completely hidden from plain sight.

I agree it's probably an edge-case; I expected it to work with tuples though, but your explanation makes complete sense, thank you.

@kirkwaiblinger kirkwaiblinger added triage Waiting for maintainers to take a look and removed awaiting response Issues waiting for a reply from the OP or another party labels Apr 22, 2024
@kirkwaiblinger kirkwaiblinger added the working as intended Issues that are closed as they are working as intended label May 7, 2024
@kirkwaiblinger kirkwaiblinger closed this as not planned Won't fix, can't repro, duplicate, stale May 7, 2024
@auvred auvred removed the triage Waiting for maintainers to take a look label May 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin working as intended Issues that are closed as they are working as intended
Projects
None yet
Development

No branches or pull requests

4 participants