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

Enhancement: [no-floating-promises] Option to only check within async functions #9061

Closed
4 tasks done
NotWoods opened this issue May 7, 2024 · 6 comments
Closed
4 tasks done
Labels
awaiting response Issues waiting for a reply from the OP or another party enhancement: plugin rule option New rule option for an existing eslint-plugin rule package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@NotWoods
Copy link
Contributor

NotWoods commented May 7, 2024

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/no-floating-promises/

Description

no-floating-promises does two related things:

  • Help remember to await promises inside async functions and promise chains
  • Make sure error from promises are caught when they're call from synchronous code

Because no-floating-promises has no good way to check if errors are caught in a function (ie using try-catch), the requirement to add .catch everywhere has encouraged developers to find workarounds like adding empty catch handlers that suppress other checks like the unhandled promise rejection event. We've decided to disable the rule in our codebase and push developers to look at unhandled promise rejections errors in telemetry instead.

However, it would still be nice to keep a version of the rule that helps us remember to use await inside async functions. I'd like to see a version of the rule that only runs on promises inside async functions (and maybe runs on promises inside a .then chain) and ignores promises called from synchronous functions like React.useEffect.

Fail

async function() {
  Promise.resolve()
}

Promise.resolve().then(() => {
  Promise.resolve()
})

Pass

async function() {
  await Promise.resolve()
}

Promise.resolve().then(() => {
  return Promise.resolve()
})

function() {
  Promise.resolve()
}

Additional Info

No response

@NotWoods NotWoods 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 maintainers to take a look labels May 7, 2024
@bradzacher
Copy link
Member

Because no-floating-promises has no good way to check if errors are caught in a function (ie using try-catch)

I'm not quite sure I understand what you mean by this. Could you give an example?


Have you considered the ignoreVoid option?
https://typescript-eslint.io/rules/no-floating-promises/#ignorevoid

It adds a declarative way for a developer to declare "I am intentionally not awaiting this promise" and it works in both sync and async contexts.

A lot of people like it because it is "symmetrical" with the await.


Personally I don't think this is a good option because often times you can create a promise and forget to await it, even in a sync context. If you ignore all promises in sync contexts then it's very easy to create floating promises accidentally - as in - you meant for it to be an async function but you forgot to do. So.

That is a massive bug vector in code!
OTOH if you leverage void or disable comments or some bespoke internal thing - you can enforce the floating promise is explicitly handled as opposed to implicitly handled or forgotten.

@bradzacher bradzacher 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 May 7, 2024
@NotWoods
Copy link
Contributor Author

NotWoods commented May 8, 2024

Unfortunately we have some developers that will see any tool we give as a hammer so I don't want to introduce void 😅 Already I've seen one promise-inside-promise nested code with 5 nested .catch(() => {}) instead of just using a flat promise chain. That's our current massive bug vector and I've talked with Josh a couple times and haven't found a good strict way to avoid this. We're currently choosing to turn off the rule because we'd prefer seeing bugs show up as unhandled promises rather than swallowing with catch.

(I guarantee if we turn on a no-empty-catch rule people will start just copy-pasting comments. I previously opened an issue because some devs write promise.catch; as a workaround).

Could you give an example?

async function foo() {
  try {
    await somePromise;
  } catch (error) {
    logger.log(error);
  }
}

// needs .catch or void to satisfy linter even though this code should be safe
foo();

@bradzacher
Copy link
Member

async function foo() {
  try {
    await somePromise;
  } catch (error) {
    logger.log(error);
  }
}

// needs .catch or void to satisfy linter even though this code should be safe
foo();

What happens when logger.log throws an unhandled exception 😬

Jokes aside - #8404 is an enhancement that would allow you to build tooling for "guaranteed infallible promises" and have the rule understand that.


previously opened an issue because some devs write promise.catch; as a workaround

no-unused-expressions is your friend for preventing that!


Unfortunately we have some developers that will see any tool we give as a hammer so I don't want to introduce void 😅

I'm not sure I understand - how would "allow floating promises in sync contexts" be any better than "use void to mark floating promises in sync contexts"? It seems like it's worse?


At meta the pattern we used was a function like so:

function promiseDone(p: Promise<any>): void {
  p.catch(someGlobalPromiseErrorLogger);
}

useEffect(() => {
  promiseDone(Promise.resolve());
});

So like void it was clear and explicit that the intent was to not await the promise, and it also had the added benefit of ensuring the promise had error handling attached.

@NotWoods
Copy link
Contributor Author

NotWoods commented May 8, 2024

Thanks for showing the pattern from meta, that's good to know and something to investigate for us.

no-unused-expressions is your friend for preventing that!

We've turned this on since :) It solved that workaround but now unfortunately developers just use empty catch.

I'm not sure I understand - how would "allow floating promises in sync contexts" be any better than "use void to mark floating promises in sync contexts"? It seems like it's worse?

I don't want to allow using void in a context where await would work, such as an async function or promise chain. A developer unfamiliar with the rule will not know what void is doing and not think twice about copy-pasting it. That might be an improvement over no flag at all so it's something we can experiment with. When I talked to Josh about this previously it seemed like this was a legacy option so I wasn't planning to use it.

@bradzacher
Copy link
Member

bradzacher commented May 8, 2024

When I talked to Josh about this previously it seemed like this was a legacy option so I wasn't planning to use it.

It's not! Many codebases use it because it provides that nice, self-documenting, no-disable-comment way to mark a promise as intentionally floating.

Josh calls it legacy because he doesn't like it and it originally became a part of the rule by accident. Way way back in the day someone did it by accident and asked for an option to catch it. And when it was added people learned it and liked it and so it became a common style.

At Canva we use it and people like it because it means no disable comments and no empty catches.


I think you're ultimately thinking about this too defensively.

A developer unfamiliar with the rule will not know what void is doing and not think twice about copy-pasting it.

You could replace void with literally any concept and have the same outcome! Disable comments, consuming functions, empty catches, no-call catches, etc. Even applies to any rule really as all rules enforce specific styles that people may copy and paste and misuse.

People will need to learn your codebase style and convention - there's likely many patterns within your codebases that are considered idiomatic style internally, but that are not used externally. It's a very common phenomenon in codebases as they evolve naturally!

Put it this way:
If someone blindly copy+pastes a voided promise to an async function and doesn't switch it to an await - they 100% would also copy paste an empty catch promise without awaiting or any other form really.

From my experience working within large react codebases (fb and canva) - you're more likely to have people forget to make a function async than you are to have people misuse the "anti-floating" tool. And both result in the same problem - that there is a floating promise - but the former is harder to track down because you need to find an unannotsted, floating promise. Whereas the void approach is very clear - just look for the void!


All that being said - I think ultimately what you want is #8404. You want to mark a promise as infallible and that is what that issue will allow you to do.

Ignoring promises in sync contexts is just a workaround for the issue of the rule reporting on infallible promises.

@NotWoods
Copy link
Contributor Author

NotWoods commented May 9, 2024

Sounds good, I'll explore using void in our codebase instead. I think it would be useful to have a rule/option to prevent it from being used in async functions but that's a different issue to this, so I'll close it :)

@NotWoods NotWoods closed this as completed May 9, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 17, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
awaiting response Issues waiting for a reply from the OP or another party enhancement: plugin rule option New rule option for an existing eslint-plugin rule package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

No branches or pull requests

2 participants