Skip to content

Configs: [return-await] Change default option from in-try-catch to always #10165

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
2 tasks done
Zamiell opened this issue Oct 17, 2024 · 10 comments
Open
2 tasks done
Labels
breaking change This change will require a new major version to be released package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin preset config change Proposal for an addition, removal, or general change to a preset config

Comments

@Zamiell
Copy link
Contributor

Zamiell commented Oct 17, 2024

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

Description

In the documentation for the @typescript-eslint/return-await rule, it says that using "in-try-catch" as opposed to "always" is purely a stylistic concern.

However, on this mdn page, it explains in the "Improving stack trace" section that using the "in-try-catch" style results in a worse stack trace. Thus, if true, this is not purely a stylistic concern.

Is this accurate? If so, it seems a compelling reason to use "always" instead of "in-try-catch", and I propose that:

  1. the documentation for the return-await rule should be updated to reflect this and
  2. the default option should be updated for all users of TSESLint.
@Zamiell Zamiell added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin preset config change Proposal for an addition, removal, or general change to a preset config triage Waiting for team members to take a look labels Oct 17, 2024
@kirkwaiblinger
Copy link
Member

I'm +1, but note

@JoshuaKGoldberg
Copy link
Member

🤔 It is kind of unfortunate that the rule has two different areas of use:

  • Logical: error-handling contexts
  • Stylistic: ordinary contexts

I wonder if should split the rule in two, so that the logical use could go into recommendedTypeChecked or strictTypeChecked and the stylistic use in stylisticTypeChecked...

My issue with setting 'always' as a default is that return await promise; is arguably worse than return promise; for readability. It's just a stylistic consistency point. And in opting for more consistency we end up with more code. Which is especially annoying for developers like me who very rarely use try/catch (Try...Catch As Little As Possible).

@JoshuaKGoldberg
Copy link
Member

oh and - cc @kirkwaiblinger, since you've done a bunch with async/await rules, this is probably in your area 😄

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Dec 7, 2024

🤔 It is kind of unfortunate that the rule has two different areas of use:

  • Logical: error-handling contexts
  • Stylistic: ordinary contexts

TBH, we say this, but I don't fully agree. It's like saying that no-floating-promises is purely stylistic as long as you don't write functions that throw exceptions. The pattern being reported is always a bug vector, whether or not you think you're going to trigger the bug. While we have the luxury of being able to syntactically detect the usages of return promise that are a clear and present danger (under current language semantics, which can change and force users to write clearly and presently dangerous code, a la #7889), I don't think that makes the other return promise usages "safe".

I wonder if should split the rule in two...

👎

...so that the logical use could go into recommendedTypeChecked or strictTypeChecked and the stylistic use in stylisticTypeChecked...

We could already do this with the "errorHandlingCorrectnessOnly" option in recommendedTypeChecked (which is right now the case in strictTypeChecked) and "always"/"in-try-catch" in stylisticTypeChecked.

My issue with setting 'always' as a default is that return await promise; is arguably worse than return promise; for readability.

I'd argue the opposite! 😁 Having worked in codebases with lots of async code using the in-try-catch style and then switched to the always style, I have never looked back. The await is an extremely useful visual marker that something async is happening. I can come up with examples of where this matters IMO. And I hold this opinion independently of whether exceptions are ever used in the codebase.


I've tried to be concise, but I have plenty more thoughts and justifications for these opinions that I can produce if there's desire to dive deeper.

@JoshuaKGoldberg
Copy link
Member

plenty more thoughts and justifications for these opinions that I can produce if there's desire to dive deeper.

I personally agree that await is an extremely useful visual marker, etc. - but that's in the stylistic realm. I think in order to change the rule's setting in recommended or strict, we'll need to justify it from a purely logical, non-stylistic perspective.

Which I think is hampered a bit from the rule's docs:

Ordinary contexts are anywhere else a promise may be returned. The choice of whether to await a returned promise in an ordinary context is mostly stylistic.

Other than reducing ticks from 2 to 1 (a negligible performance change), do we have actual runtime benefits we can demonstrate from outside the error handling context?

@Zamiell
Copy link
Contributor Author

Zamiell commented Dec 15, 2024

from outside the error handling context?

The way you phrased this makes it seem like improved error-handling is not enough on its own and we need some other, separate reason. But is that true?

I think the point of the MDN article I linked in the OP is that, in JavaScript/TypeScript land, you don't know if any arbitrary function that you call will throw. Thus, you are always have to worry about errors. Thus, in order to improve the (potential) stack trace, you would always want to include await, just in case. (And the cost of writing await is lessened if the rule automatically adds it for you via the auto-fixer.)

@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented Dec 15, 2024

By "error handling context" I mean what the rule's docs mean: specifically code inside a try/catch (or something like a using that turns into one). That includes the code inside the following middle() function, but not the outer() function:

async function inner() {
  throw new Error("Hi.");
}

async function middle() {
  try {
    return /* await */ inner();
  } catch (error) {
    console.log("Caught in middle:", error);
  }
}

async function outer() {
  return /* await */ middle();
}

await outer();

Outputs from un-commenting 0, 1, or 2 of the /* await */s:

middle outer Stack Mentions
/* await */ /* await */ file:/// ... inner middle outer
/* await */ await file:/// ... inner middle outer
await /* await */ Caught in middle: ... inner middle outer
await await Caught in middle: ... inner middle outer

Notice how whether outer having an await didn't change what the stack mentioned. That's what I mean by the difference outside an error handling context seeming to be purely stylistic. When the context is a function and not a try or equivalent, it seems to me that the main benefit is stylistic consistency.

@kirkwaiblinger
Copy link
Member

Huh - apparently the cryptic await null; is an essential part of the MDN demo, which surprises me. But I'm thinking maybe the stack trace behaves differently depending whether the error occurs during the first, synchronous async frame of the async function or during a subsequent, asynchronous async frame. Seems like this will be @Josh-Cena's area of expertise 😀

@kirkwaiblinger
Copy link
Member

Zooming out a bit, I think it's fair to say that it's a lot easier to justify that always is the best option than it is to justify changing the default from in-try-catch to always

  • The stack trace difference is nice, but it really isn't a terribly strong factor.
  • To @JoshuaKGoldberg's point, control flow bugs can only occur in syntactically analyzable error-handling contexts, which are handled the same in both options (but, more on this below).

Let's suppose for the sake of argument that we agree that always would be the default option if we created the rule completely from scratch today. Then, what I see as the strongest reasons in favor of changing the current default to always are:

  1. It's much easier to communicate. The rule docs are pretty rough right now. It would be so much easier to treat the reader happy path as simply "Only return values, never promises, from async functions.". Then all the complexity and caveats can be sequestered to text intended for ultra-highly motivated users to say "If you prefer other patterns for stylistic reasons, we also support that, but here's the limitations about when it's even safe to do so, and here's what you'll give up (stack trace edge cases) by preferring that style, and FYI, no, it won't help code run faster, performance is at best equal."

  2. The always option is much less susceptible to implementation bugs than in-try-catch. The rule does have a history of implementation bugs around the detection of error-handling contexts, not all of which were uncontroversial to fix:

    3 of 4 of these issues did not affect users of always.

@kirkwaiblinger
Copy link
Member

Had a chat with @JoshuaKGoldberg and it sounded like he was swayed. The understood plan of action was:

  • a change to the default option, and, accordingly, a docs change. This would be a breaking change in the next major version.
  • no change to the behavior in our recommended-type-checked (which doesn't include return-await at all) or strict-type-checked (which enables return-await with the explicitly specified error-handling-correctness-only option) preset configs.

@JoshuaKGoldberg JoshuaKGoldberg removed the triage Waiting for team members to take a look label Dec 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change This change will require a new major version to be released package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin preset config change Proposal for an addition, removal, or general change to a preset config
Projects
None yet
Development

No branches or pull requests

3 participants