Skip to content

docs: a long awaited blog post #11118

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions packages/website/blog/2025-04-29-promises.md
Original file line number Diff line number Diff line change
@@ -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).
6 changes: 6 additions & 0 deletions packages/website/blog/authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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