Skip to content

Bug: [@typescript-eslint/no-floating-promises] Not catching promises assigned to a variable #10822

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

Closed
4 tasks done
jlaustill opened this issue Feb 10, 2025 · 8 comments
Closed
4 tasks done
Labels
duplicate This issue or pull request already exists locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@jlaustill
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=4.8.4&fileType=.ts&code=PQKgBApgzgNglgOwC5gAJIJ4AdoGMBOcWSAtNPMsAgPYkC2cUArlBACYlb7UOtQBcYAOQR83fELAhgAKFCRYiFOmx5CxMoso0SAMxjUAhkkQBzTt17RBIsdQlTZM3NQRQUbagGUeEJAAszAEEoDARcMABeMENQ8LAACgBKKIA%2BMABvGTAwfD8mfAQwAAVLRggAOjyoahgANwgE5LTM7Jzc-MKwACIAETg2MBq6P0CEU26AbjaAXySZGZlnV3cwUz8fEYCzKLBPTdHguNxkybBgYDAvf2omGEHRcSHEXAg1jd9t8bBGMAQIBr4MD%2BQwINgwdhLfafMamEJhE5JabLNy1SoGUwJboAdX8GD%2B1DA%2BiMJm%2BXB45UgdnwAH5ugAad5IA5fUxIpZAA&eslintrc=N4KABGBEBOCuA2BTAzpAXGUELjAXxDyA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false

Repro Code

/* eslint @typescript-eslint/no-misused-promises: 'error' */
/* eslint @typescript-eslint/no-floating-promises: 'error' */

const doSomethingAsync = async () => {
  return Promise.resolve(() => {
    return "Did something";
  })
}

const getSomething = doSomethingAsync(); // Should error since getSomething is never handled

doSomethingAsync();

console.log("Why no floating promise error?", getSomething);

ESLint Config

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

tsconfig

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Expected Result

I expected that line 10 would get a no-floating-promises error since the promise doesn't meet any of these criteria

Calling its .then() with two arguments
Calling its .catch() with one argument
awaiting it
returning it
voiding it

Actual Result

No error is reported

Additional Info

We just upgraded from eslint 8 to 9 and I'm kind of shocked to see this not working. It's one of the most common issues I've seen in jest tests and poorly typed code. Am I missing something? Is there another rule meant to catch this promise?

@jlaustill jlaustill added bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Feb 10, 2025
@JoshuaKGoldberg
Copy link
Member

Duplicate of #3359, #5749, and #6766. Assigning a variable to a Promise is one of the valid ways to handle it.

Is there another rule meant to catch this promise?

Yup, there is. @typescript-eslint/no-unused-vars detects unused variables like that one.

Linking this to #8088, which tracks us overhauling the no-floating-promises docs and report messages. I suspect this is a commonly enough asked question that it would make sense to remember to mention it there (cc @kirkwaiblinger).

@JoshuaKGoldberg JoshuaKGoldberg added duplicate This issue or pull request already exists and removed bug Something isn't working triage Waiting for team members to take a look labels Feb 10, 2025
@JoshuaKGoldberg JoshuaKGoldberg changed the title Bug: [@typescript-eslint/no-floating-promises] Not catching the most basic floating promises Bug: [@typescript-eslint/no-floating-promises] Not catching promises assigned to a variable Feb 10, 2025
@jlaustill
Copy link
Author

Duplicate of #3359, #5749, and #6766. Assigning a variable to a Promise is one of the valid ways to handle it.

Is there another rule meant to catch this promise?

Yup, there is. @typescript-eslint/no-unused-vars detects unused variables like that one.

Linking this to #8088, which tracks us overhauling the no-floating-promises docs and report messages. I suspect this is a commonly enough asked question that it would make sense to remember to mention it there (cc @kirkwaiblinger).

Assigning it to a variable is NOT handling it. no-unused-vars isn't going to help because it our real world examples the variables are used. I did this as a minimal reproduction. The linter used to be smart enough to check whether or not the variable was ever awaited, why take that away? I've got to be missing something :(

@kirkwaiblinger
Copy link
Member

Hmm, I thought the docs called that out more clearly but yeah I'm not seeing anything obvious. Maybe we lost a mention during previous docs changes? The closest we have now is the Tip which says "no-floating-promises only detects apparently unhandled Promise statements. See no-misused-promises for detecting code that provides Promises to logical locations such as if statements."

@jlaustill
Copy link
Author

Between this and the disastrous flat file change, I guess it's time to start digging into Biomejs. Thanks for the quick response y'all!

@kirkwaiblinger
Copy link
Member

Assigning it to a variable is NOT handling it.

It's true. It's important to think of no-floating-promises as a heuristic that helps avoid floating promises, not a guarantee that honoring its reports can guarantee no promises ever float. The assumption the rule makes when you assign to a variable or class field is that you'll do something useful with that variable. There's a limit to how deeply we could analyze what you do with that variable. For instance, some function calls handle the promise, some don't

const promise = Promise.reject('foo');
console.log(promise); // doesn't handle the promise
await Promise.all([promise]); // does handle the promise

function terminatePromise(x: Promise<unknown>): void {
   x.catch(e => console.error(e);
}

terminatePromise(promise); // does handle the promise

To keep the rule logic understandable for authors and for users, its purview is limited to looking for suspicious promise-valued statements, rather than attempting to analyze what you do with variables/expressions.

@JoshuaKGoldberg
Copy link
Member

NOT handling it.

We're defining "handling" differently 😄. From the rule's perspective, putting it in a variable that is then used satisfies that criteria.

From the perspective of the linter, as long as something uses the variable, it's very hard to know whether that variable is appropriately handled as a Promise.

At first glance it seems like a straightforward heuristic could be "is the variable awaited?" (#3359 (comment)). But consider these functions:

declare function doWork(): Promise<void>;
async function mainReturns() {
  const work = doWork();

  return work;
}
declare function processInQueue(task: Promise<void>): void;

async function mainProcesses() {
  const work = doWork();

  processInQueue(task);
}
declare function log(data: unknown): void;

async function mainLogs() {
  const work = doWork();

  log(task);
}

Each could be argued either way:

  • mainReturns gives back the promise, so as long as it itself isn't floating, the code does "handle" the variable
  • mainProcesses: presumably the processInQueue function puts the variable in a queue, right?...
  • mainLogs: ...is exactly the same as the queue example, but with a different function name and parameter typed as unknown instead of Promise<void>. It's looks more like console.log, which arguably means the Promise isn't handled correctly... or is it? Maybe just logging a task is the intended handling?

because it our real world examples the variables are used. I did this as a minimal reproduction

Can you share a motivating example? I'd love to take a look - even if it can't be pared down to a truly "minimal" reproduction easily.

The linter used to be smart enough to check whether or not the variable was ever awaited, why take that away?

Unless there's a not-yet-known bug, I don't think it ever was? Looking through https://github.com/typescript-eslint/typescript-eslint/blob/main/CHANGELOG.md, nothing in recent years pops out to me as changing how it handles variables within functions. But if you can provide a complex reproduction showing an older version missing this, we can take a deeper look at the potential regression.

time to start digging into Biomejs. Thanks for the quick response y'all!

Your timing is good, they just shipped their own version of no-floating-promises: biomejs/biome#4956 -> biomejs/biome#4911. I don't see it on their site yet, but that should come soon. Filed biomejs/website#1732 in the meantime to ask (I'm sure I'm missing something myself).

That being said, I'm pretty sure Biome's no-floating-promises equivalent also doesn't flag variables 😄. It only reports statements such as function calls: https://github.com/biomejs/biome/pull/4911/files#diff-fa08cc1e3d0164c82fa5c7869713b76567a409e870a46a4abaaae45b581f8507R23.

@kirkwaiblinger
Copy link
Member

Between this and the disastrous flat file change, I guess it's time to start digging into Biomejs. Thanks for the quick response y'all!

Good luck with this. The flat config break was upstream in eslint core, a separately maintained project, and no-floating-promises is the canonical example of what typescript-eslint can do that native code linters cannot do thoroughly due to technical limitations (see the roadmap, though - they have POCd a decent non-typed heuristic-based approach).

@jlaustill
Copy link
Author

Thanks @JoshuaKGoldberg ! I'm 99.99 percent sure I have a case of hanging tests because something async isn't handled somewhere. I'm just trying to find it :) Without lint/compiler help, I'm down to removing every test and adding them back one at a time, but it is what it is :)

I also realize that the flat config was upstream eslint core. I also know that I have used eslint since v1 and it's always been a pleasure to use, helped me find bugs, and never got in my way until v8. These days I'm wasting so much time on it :( I'll maybe end up running eslint/biomejs side by side for awhile and see how it goes. I just need to get back to doing real work, not spending so much time on this :)

@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Feb 18, 2025
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 18, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
duplicate This issue or pull request already exists locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

No branches or pull requests

3 participants