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..ae1448dc0170 --- /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). 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