From d2a7ac41a730b70afbad9606e5bf3dfe48109fa4 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:08:42 -0600 Subject: [PATCH 1/3] WIP --- packages/website/blog/2025-01-02-promises.md | 165 +++++++++++++++++++ packages/website/blog/authors.yml | 6 + 2 files changed, 171 insertions(+) create mode 100644 packages/website/blog/2025-01-02-promises.md diff --git a/packages/website/blog/2025-01-02-promises.md b/packages/website/blog/2025-01-02-promises.md new file mode 100644 index 000000000000..8c9b6cce6e29 --- /dev/null +++ b/packages/website/blog/2025-01-02-promises.md @@ -0,0 +1,165 @@ +--- +authors: kirkwaiblinger +description: fooooo +slug: promises +tags: [] +title: Async, Promises, Thenables +--- + +We have a few fundamental principles + +- Async code should only be used when needed +- Unhandled rejections are Bad. + +This post covers + +- [@typescript-eslint/await-thenable](/rules/await-thenable) +- [@typescript-eslint/no-floating-promises](/rules/no-floating-promises) +- [@typescript-eslint/no-misused-promises](/rules/no-misused-promises) +- [@typescript-eslint/prefer-promise-reject-errors](/rules/prefer-promise-reject-errors) +- [@typescript-eslint/promise-function-async](/rules/promise-function-async) +- [@typescript-eslint/return-await](/rules/return-await) + +## + +Imagine someone adds the following JS file in a code review. + +```js +function doSomething() { + // ... +} + +function doSomethingElse() { + // ... +} + +function getSomeValue() { + // ... +} + +export function foo() { + doSomething(); + doSomethingElse(); + return getSomeValue(); +} +``` + +Do you spot anything wrong with this snippet? Me neither! +This is perfectly plausible code as written. + +Now, let's reveal some implementations: + +```js +function doSomething() { + fetch('https://example.com', { method: 'POST' }); +} + +function doSomethingElse() { + fetch('https://example.com', { method: 'PUT' }); +} + +function getSomeValue() { + return fetch('https://example.com', { method: 'GET' }); +} + +export function foo() { + doSomething(); + doSomethingElse(); + return getSomeValue(); +} +``` + +Well, now I'm suspicious! Did the author really intend to kick off two write requests, and return the result of a read request? +Much more plausible is that they meant to kick off the write requests in parallel, and, after they're done, read the result to ensure it's correct. + +How can we catch these errors that depend on the implementations of other functions? +The answer, [of course](./2024-09-30-typed-linting.md), is TypeScript types! + +Let's take a look at what the linter tells us: + +```ts +function doSomething() { + // [@typescript-eslint/no-floating-promises]: 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. + fetch('https://example.com', { method: 'POST' }); +} +``` + +Fair enough! We've just kicked off a task but not given anyone a way to know when it's finished. +We'd best return the `fetch` promise. + +```ts +// [@typescript-eslint/promise-function-async]: Functions that return promises must be async. +function doSomething() { + return fetch('https://example.com', { method: 'POST' }); +} +``` + +Another complaint! Let's make that function `async`. +Not only is it now unambiguously clear in code review that this is async code, +the `async` keyword unlocks the `await` keyword. +And come to think of it, we'd better use that in order to make sure our request succeeded. + +```ts +async function doSomething() { + const res = await fetch('https://example.com', { method: 'POST' }); + return res.ok; +} +``` + +Following the linter complaints, we might end up in a state that looks like this: + +```ts +async function doSomething() { + const res = await fetch('https://example.com', { method: 'POST' }); + return res.ok; +} + +async function doSomethingElse() { + const res = await fetch('https://example.com', { method: 'PUT' }); + return res.ok; +} + +async function getSomeValue() { + const res = await fetch('https://example.com', { method: 'GET' }); + return res.text(); +} + +export async function foo() { + // @typescript-eslint/no-floating-promises reports this line + doSomething(); + // @typescript-eslint/no-floating-promises reports this line + doSomethingElse(); + return getSomeValue(); +} +``` + +And now we're at a point where we can actually fix the bug: + +```ts +export async function foo() { + // run these tasks in parallel + const results = await Promise.all([doSomething(), doSomethingElse()]); + // make sure they succeeded! + if (!results.every(res => res)) { + throw new Error('write requests failed!'); + } + + // validate the outcome + return getSomeValue(); +} +``` + +These are the benefits unlocked by our Promise rules. + +Their purposes are roughly as follows. See the individual rule docs for much more detailed writeups on each: + +- [@typescript-eslint/await-thenable](/rules/await-thenable): Ensure you don't `await` values that cannot be a promise +- [@typescript-eslint/no-floating-promises](/rules/no-floating-promises): Ensure that you don't forget to `await` statements that may need it. +- [@typescript-eslint/no-misused-promises](/rules/no-misused-promises): Similar to previous, but ensures you don't provide promises to locations that don't expect promises. +- [@typescript-eslint/prefer-promise-reject-errors](/rules/prefer-promise-reject-errors): Ensure that you follow best practices around error handling in `async` contexts. +- [@typescript-eslint/promise-function-async](/rules/promise-function-async): Ensure that you use `async` syntax. +- [@typescript-eslint/return-await](/rules/return-await): Prevent subtle control flow errors in `async` functions. + +Sometimes, you're in a situation where you know that a promise is safe to float. For this reason we have the `typeOrValue` specifier format. `node:test`, `fastify`. diff --git a/packages/website/blog/authors.yml b/packages/website/blog/authors.yml index 752c88f6683f..881523716c14 100644 --- a/packages/website/blog/authors.yml +++ b/packages/website/blog/authors.yml @@ -9,3 +9,9 @@ joshuakgoldberg: name: Josh Goldberg title: typescript-eslint Maintainer url: https://github.com/JoshuaKGoldberg + +kirkwaiblinger: + image_url: /img/team/kirkwaiblinger.jpg + name: Kirk Waiblinger + title: typescript-eslint Committer + url: https://github.com/kirkwaiblinger From 4a32012430c96a79e901c8446abbae61390a508d Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:44:05 -0600 Subject: [PATCH 2/3] work --- packages/website/blog/2025-01-02-promises.md | 165 -------------- packages/website/blog/2025-04-29-promises.md | 224 +++++++++++++++++++ 2 files changed, 224 insertions(+), 165 deletions(-) delete mode 100644 packages/website/blog/2025-01-02-promises.md create mode 100644 packages/website/blog/2025-04-29-promises.md diff --git a/packages/website/blog/2025-01-02-promises.md b/packages/website/blog/2025-01-02-promises.md deleted file mode 100644 index 8c9b6cce6e29..000000000000 --- a/packages/website/blog/2025-01-02-promises.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -authors: kirkwaiblinger -description: fooooo -slug: promises -tags: [] -title: Async, Promises, Thenables ---- - -We have a few fundamental principles - -- Async code should only be used when needed -- Unhandled rejections are Bad. - -This post covers - -- [@typescript-eslint/await-thenable](/rules/await-thenable) -- [@typescript-eslint/no-floating-promises](/rules/no-floating-promises) -- [@typescript-eslint/no-misused-promises](/rules/no-misused-promises) -- [@typescript-eslint/prefer-promise-reject-errors](/rules/prefer-promise-reject-errors) -- [@typescript-eslint/promise-function-async](/rules/promise-function-async) -- [@typescript-eslint/return-await](/rules/return-await) - -## - -Imagine someone adds the following JS file in a code review. - -```js -function doSomething() { - // ... -} - -function doSomethingElse() { - // ... -} - -function getSomeValue() { - // ... -} - -export function foo() { - doSomething(); - doSomethingElse(); - return getSomeValue(); -} -``` - -Do you spot anything wrong with this snippet? Me neither! -This is perfectly plausible code as written. - -Now, let's reveal some implementations: - -```js -function doSomething() { - fetch('https://example.com', { method: 'POST' }); -} - -function doSomethingElse() { - fetch('https://example.com', { method: 'PUT' }); -} - -function getSomeValue() { - return fetch('https://example.com', { method: 'GET' }); -} - -export function foo() { - doSomething(); - doSomethingElse(); - return getSomeValue(); -} -``` - -Well, now I'm suspicious! Did the author really intend to kick off two write requests, and return the result of a read request? -Much more plausible is that they meant to kick off the write requests in parallel, and, after they're done, read the result to ensure it's correct. - -How can we catch these errors that depend on the implementations of other functions? -The answer, [of course](./2024-09-30-typed-linting.md), is TypeScript types! - -Let's take a look at what the linter tells us: - -```ts -function doSomething() { - // [@typescript-eslint/no-floating-promises]: 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. - fetch('https://example.com', { method: 'POST' }); -} -``` - -Fair enough! We've just kicked off a task but not given anyone a way to know when it's finished. -We'd best return the `fetch` promise. - -```ts -// [@typescript-eslint/promise-function-async]: Functions that return promises must be async. -function doSomething() { - return fetch('https://example.com', { method: 'POST' }); -} -``` - -Another complaint! Let's make that function `async`. -Not only is it now unambiguously clear in code review that this is async code, -the `async` keyword unlocks the `await` keyword. -And come to think of it, we'd better use that in order to make sure our request succeeded. - -```ts -async function doSomething() { - const res = await fetch('https://example.com', { method: 'POST' }); - return res.ok; -} -``` - -Following the linter complaints, we might end up in a state that looks like this: - -```ts -async function doSomething() { - const res = await fetch('https://example.com', { method: 'POST' }); - return res.ok; -} - -async function doSomethingElse() { - const res = await fetch('https://example.com', { method: 'PUT' }); - return res.ok; -} - -async function getSomeValue() { - const res = await fetch('https://example.com', { method: 'GET' }); - return res.text(); -} - -export async function foo() { - // @typescript-eslint/no-floating-promises reports this line - doSomething(); - // @typescript-eslint/no-floating-promises reports this line - doSomethingElse(); - return getSomeValue(); -} -``` - -And now we're at a point where we can actually fix the bug: - -```ts -export async function foo() { - // run these tasks in parallel - const results = await Promise.all([doSomething(), doSomethingElse()]); - // make sure they succeeded! - if (!results.every(res => res)) { - throw new Error('write requests failed!'); - } - - // validate the outcome - return getSomeValue(); -} -``` - -These are the benefits unlocked by our Promise rules. - -Their purposes are roughly as follows. See the individual rule docs for much more detailed writeups on each: - -- [@typescript-eslint/await-thenable](/rules/await-thenable): Ensure you don't `await` values that cannot be a promise -- [@typescript-eslint/no-floating-promises](/rules/no-floating-promises): Ensure that you don't forget to `await` statements that may need it. -- [@typescript-eslint/no-misused-promises](/rules/no-misused-promises): Similar to previous, but ensures you don't provide promises to locations that don't expect promises. -- [@typescript-eslint/prefer-promise-reject-errors](/rules/prefer-promise-reject-errors): Ensure that you follow best practices around error handling in `async` contexts. -- [@typescript-eslint/promise-function-async](/rules/promise-function-async): Ensure that you use `async` syntax. -- [@typescript-eslint/return-await](/rules/return-await): Prevent subtle control flow errors in `async` functions. - -Sometimes, you're in a situation where you know that a promise is safe to float. For this reason we have the `typeOrValue` specifier format. `node:test`, `fastify`. diff --git a/packages/website/blog/2025-04-29-promises.md b/packages/website/blog/2025-04-29-promises.md new file mode 100644 index 000000000000..0ab43d89e5aa --- /dev/null +++ b/packages/website/blog/2025-04-29-promises.md @@ -0,0 +1,224 @@ +--- +authors: kirkwaiblinger +description: How typescript-eslint's suite of Promise rules enforce best practices around async code +slug: promises +tags: [] +title: 'Await For It: Writing Reliable Async Code' +--- + +When working with asynchronous code, we have a few fundamental principles. + +- Async code should only be used when needed +- Unhandled promise rejections are Bad. +- Control flow should be explicit. + +Let's see our promise rules in action on some sample code that might come up during the development cycle, and see how they enforce our core principles. + +## Worked Example + +Imagine someone adds the following JS file in a code review. + +```js +function doSomething() { + fetch('https://example.com', { method: 'POST' }); +} + +function doSomethingElse() { + fetch('https://example.com', { method: 'PUT' }); +} + +function getSomeValue() { + return fetch('https://example.com', { method: 'GET' }); +} + +export function foo() { + doSomething(); + doSomethingElse(); + return getSomeValue(); +} +``` + +This code seems suspicious! +Did the author really intend to kick off two write requests and a read request, then just return the result of a read request? +Much more plausible is that they meant to kick off the write requests in parallel, and, after they're done, read the result to ensure it's correct. + +How can we catch these errors that depend on the implementations of other functions? +The answer, [of course](./2024-09-30-typed-linting.md), is TypeScript types! + +Let's take a look at what the linter tells us: + +```ts +function doSomething() { + // [@typescript-eslint/no-floating-promises]: 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. + fetch('https://example.com', { method: 'POST' }); +} +``` + +Fair enough! +We've just kicked off a task but not given anyone a way to know when it's finished. +We'd best return the `fetch` promise. + +```ts +// [@typescript-eslint/promise-function-async]: Functions that return +// promises must be async. +function doSomething() { + // Remove this line + fetch('https://example.com', { method: 'POST' }); + // Add this line + return fetch('https://example.com', { method: 'POST' }); +} +``` + +But not we have another complaint! Let's make that function `async`. +Not only is it now unambiguously clear in code review that this is async code, the `async` keyword unlocks the `await` keyword. +And, come to think of it, we'd better use that in order to make sure our request succeeded. + +```ts +async function doSomething() { + // Remove this line + return fetch('https://example.com', { method: 'POST' }); + // Added lines start + const res = await fetch('https://example.com', { method: 'POST' }); + return res.ok; + // Added lines end +} +``` + +Following the linter complaints, we might end up in a state that looks like this: + +```ts +async function doSomething() { + const res = await fetch('https://example.com', { method: 'POST' }); + return res.ok; +} + +async function doSomethingElse() { + const res = await fetch('https://example.com', { method: 'PUT' }); + return res.ok; +} + +async function getSomeValue() { + const res = await fetch('https://example.com', { method: 'GET' }); + return res.text(); +} + +export async function foo() { + // @typescript-eslint/no-floating-promises reports this line + doSomething(); + // @typescript-eslint/no-floating-promises reports this line + doSomethingElse(); + return getSomeValue(); +} +``` + +And now we're at a point where we can actually fix the suspicious control flow: + +```ts +export async function foo() { + // run these tasks in parallel + const results = await Promise.all([doSomething(), doSomethingElse()]); + // make sure they succeeded! + if (!results.every(res => res)) { + throw new Error('write requests failed!'); + } + + // validate the outcome + return getSomeValue(); +} +``` + +This type of deep analysis is just a small subset of what our Promise rules can offer, as a result of their usage of TypeScript types. + +## Promise rules + +The following table gives an overview of our core promise rules. See the individual rule docs for much more detail on each rule: + +| Rule | Description | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| [await-thenable](/rules/await-thenable) | Ensure you don't `await` values that cannot be a promise | +| [no-floating-promises](/rules/no-floating-promises) | Ensure that you don't forget to `await` statements that may need it. | +| [no-misused-promises](/rules/no-misused-promises) | Ensures you don't provide promises to locations that don't expect promises. | +| [prefer-promise-reject-errors](/rules/prefer-promise-reject-errors) | Ensure that you only reject promises with `Error` objects. | +| [promise-function-async](/rules/promise-function-async) | Ensure that you use `async` syntax. | +| [return-await](/rules/return-await) | Prevent subtle control flow errors in `async` functions and enforce consistent stylistic choices. | + +As seen in the example provided above, these rules work best in concert, rather than individually. +We recommend you enable all of them (or simply [use our strict-type-checked preset configuration](/users/configs#strict-type-checked)). Taken together, these rules help ensure predictable control flow in asynchronous code and prevent unhandled promise rejections from occurring. + +## When you know better + +While our Promise rules lay out a framework of generally applicable best practices, there are situations where valid code doesn't fit into this framework. +We don't recommend disabling the promise analysis rules entirely to handle these situations. +Instead, we recommend two main strategies for dealing with these situations. + +### Ignoring code ad-hoc + +The first strategy is to use a [standard ESLint-disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1). + +```ts +async function foo() { + // ... + + /* eslint-disable-next-line @typescript-eslint/no-floating-promises -- + * This promise is ok to float because it won't reject and we don't + * want to await it here for control flow reasons. + */ + pinkiePromise(); + + // ... +} +``` + +Don't be too shy to use disable comments! +We find that many users are averse to disabling rules, but it's an absolutely valid tool when the situation calls for it. + +:::tip + +Be sure to [include a meaningful description](https://eslint.org/docs/latest/use/configure/rules#comment-descriptions) so that you and your teammates remember why the rule is disabled! + +::: + +### Configuring rules to ignore certain patterns + +If you're using certain libraries, you may want to ignore certain patterns. +For example, the `test()` function from [`node:test`](https://nodejs.org/api/test.html) may return a promise, but it should not always be awaited + +```ts +import { describe, test } from 'node:test'; +import * as assert from 'node:assert'; + +// no-floating-promises issues a report that can be ignored in this case. +test('asynchronous top level test', async () => { + assert.ok(true); +}); +``` + +:::warning + +This example is for illustrative purposes, not an explainer on `node:test`. +If you are using `node:test`, please note that [sometimes, awaiting `test()` _is_ required](https://nodejs.org/api/test.html#subtests). + +::: + +We can configure the `no-floating-promises` rule to avoid this with a configuration like + +```json +{ + "@typescript-eslint/no-floating-promises": [ + "error", + { + "allowForKnownSafeCalls": [ + { + "from": "package", + "name": "test", + "package": "node:test" + } + ] + } + ] +} +``` + +For further reading on this type of configuration, see the [`no-floating-promises` options](/rules/no-floating-promises#options) and the docs on our [`TypeOrValueSpecifier` format](/packages/type-utils/type-or-value-specifier). From d08de9ec885d981d0a948610ccce12304d82984b Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Thu, 1 May 2025 07:41:21 -0600 Subject: [PATCH 3/3] eslint-disable --- packages/website/blog/2025-04-29-promises.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/blog/2025-04-29-promises.md b/packages/website/blog/2025-04-29-promises.md index 0ab43d89e5aa..ae1448dc0170 100644 --- a/packages/website/blog/2025-04-29-promises.md +++ b/packages/website/blog/2025-04-29-promises.md @@ -155,7 +155,7 @@ Instead, we recommend two main strategies for dealing with these situations. ### Ignoring code ad-hoc -The first strategy is to use a [standard ESLint-disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1). +The first strategy is to use a [standard eslint-disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1). ```ts async function foo() {