14 Linting Rules To Help You Write Asynchronous Code in JavaScript
Tags: javascript, beginners, webdev, productivity
Published: 2022-03-25 19:41:31

*This article was originally published at https://maximorlov.com/linting-rules-for-asynchronous-code-in-javascript/*

Debugging asynchronous code in JavaScript can feel like navigating a minefield at times. You don't know when and where the console.logs will print out, and you have no idea how your code is executed.

It's hard to correctly structure async code so it executes in the right order as you intend it to.

Wouldn't it be nice if you had some guidance while writing asynchronous code, and to get a helpful message when you're about to make a mistake?

Luckily we have linters to catch some of our bugs before we push them to production. The following is a compiled list of linting rules to specifically help you with writing asynchronous code in JavaScript and Node.js.

Even if you end up not using the rules in your project, reading their descriptions will lead to a better understanding of async code and improve your developer skills.

## ESLint rules for asynchronous code
The following rules are shipped by default with [ESLint](https://maximorlov.com/linting-rules-for-asynchronous-code-in-javascript/). Enable them by adding them to your `.eslintrc` configuration file.

### 1. no-async-promise-executor
This rule disallows passing an `async` function to the `new Promise` constructor.

```js
// ❌
new Promise(async (resolve, reject) => {});

// ✅
new Promise((resolve, reject) => {});
```

While it's technically valid to pass an asynchronous function to the Promise constructor, doing so is usually a mistake for any of these two reasons. First, if the async function throws, the error will be lost and won't be rejected by the newly-constructed promise. Second, if `await` is used inside the constructor function, the wrapping Promise might be unnecessary and you could remove it.

### 2. no-await-in-loop
This rule disallows using `await` inside loops.

When doing an operation on each element of an iterable and awaiting an asynchronous task, it's oftentimes an indication that the program is not taking full advantage of JavaScript's event-driven architecture. By [executing the tasks in parallel](https://maximorlov.com/parallel-tasks-with-pure-javascript/) instead, you could greatly improve the efficiency of your code.

```js
// ❌
for (const url of urls) {
  const response = await fetch(url);
}

// ✅
const responses = [];
for (const url of urls) {
  const response = fetch(url);
  responses.push(response);
}

await Promise.all(responses);
```

In the odd case when you deliberately want to run tasks in sequence, I recommend temporarily disabling this rule with an inline comment: `// eslint-disable-line no-await-in-loop`.

### 3. no-promise-executor-return
This rule disallows returning a value inside a Promise constructor.

```js
// ❌
new Promise((resolve, reject) => {
  return result;
});

// ✅
new Promise((resolve, reject) => {
  resolve(result);
});
```

Values returned inside a Promise constructor cannot be used and don't affect the promise in any way. The value should be passed to `resolve` instead, or if an error occurred, call `reject` with the error.

> Note: This rule won't prevent you from returning a value inside a nested callback within the Promise constructor. Always make sure to use `resolve` or `reject` to conclude a promise.

### 4. require-atomic-updates
This rule disallows assignments in combination with `await`, which can lead to race conditions.

Consider the example below, what do you think the final value of `totalPosts` will be?

```js
// ❌
let totalPosts = 0;

async function getPosts(userId) {
  const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
  await sleep(Math.random() * 1000);
  return users.find((user) => user.id === userId).posts;
}

async function addPosts(userId) {
  totalPosts += await getPosts(userId);
}

await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);
```

Perhaps you've sensed that this was a trick question and the answer is not 8. That's right, `totalPosts` prints either 5 or 3. Go ahead and try it yourself in the browser.

The issue lies in the fact that there is a time gap between reading and updating `totalPosts`. This causes a race condition such that when the value is updated in a separate function call, the update is not reflected in the current function scope. Therefore both functions add their result to `totalPosts`'s initial value of 0.

To avoid this race condition you should make sure the variable is read at the same time it's updated.

```js
// ✅
let totalPosts = 0;

async function getPosts(userId) {
  const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
  await sleep(Math.random() * 1000);
  return users.find((user) => user.id === userId).posts;
}

async function addPosts(userId) {
  const posts = await getPosts(userId);
  totalPosts += posts; // variable is read and immediately updated
}

await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);
```

### 5. max-nested-callbacks
This rule enforces a maximum nesting depth for callbacks. In other words, this rule prevents callback hell!

```js
/* eslint max-nested-callbacks: ["error", 3] */

// ❌
async1((err, result1) => {
  async2(result1, (err, result2) => {
    async3(result2, (err, result3) => {
      async4(result3, (err, result4) => {
        console.log(result4);
      });
    });
  });
});

// ✅
const result1 = await asyncPromise1();
const result2 = await asyncPromise2(result1);
const result3 = await asyncPromise3(result2);
const result4 = await asyncPromise4(result3);
console.log(result4);
```

A deep level of nesting makes code hard to read and more difficult to maintain. [Refactor callbacks to promises](https://maximorlov.com/visual-guide-refactoring-callback-functions/) and use modern async/await syntax when writing asynchronous code JavaScript.

### 6. no-return-await
This rule disallows unnecessary `return await`.

```js
// ❌
async () => {
  return await getUser(userId);
}

// ✅
async () => {
  return getUser(userId);
}
```

Awaiting a promise and immediately returning it is unnecessary since [all values returned from an `async` function are wrapped in a promise](https://maximorlov.com/understanding-async-await/#how-about-async). Therefore you can return the promise directly.

An exception to this rule is when there is a surrounding `try...catch` statement. Removing the `await` keyword will cause a promise rejection to not be caught. In this case, I suggest you assign the result to a variable on a different line to make the intent clear.

```js
// 👎
async () => {
  try {
    return await getUser(userId);
  } catch (error) {
    // Handle getUser error
  }
}

// 👍
async () => {
  try {
    const user = await getUser(userId);
    return user;
  } catch (error) {
    // Handle getUser error
  }
}
```

### 7. prefer-promise-reject-errors
This rule enforces using an `Error` object when rejecting a Promise.

```js
// ❌
Promise.reject('An error occurred');

// ✅
Promise.reject(new Error('An error occurred'));
```

It's best practice to always reject a Promise with an `Error` object. Doing so will make it easier to trace where the error came from because error objects store a stack trace.

## Node.js specific rules
The following rules are additional ESLint rules for Node.js provided by the [`eslint-plugin-node` plugin](https://github.com/mysticatea/eslint-plugin-node). To use them, you need to install and add the plugin to the `plugins` array in your `.eslintrc` configuration file.

### 8. node/handle-callback-err
This rule enforces error handling inside callbacks.

```js
// ❌
function callback(err, data) {
  console.log(data);
}

// ✅
function callback(err, data) {
  if (err) {
    console.log(err);
    return;
  }

  console.log(data);
}
```

In Node.js it's common to pass the error as the first parameter to a callback function. Forgetting to handle the error can result in your application behaving strangely.

This rule is triggered when the first argument of a function is named `err`. In large projects, it's not uncommon to find different naming variations for errors such as `e` or `error`. You can change the default configuration by providing a second argument to the rule in your `.eslintrc` file: `node/handle-callback-err: ["error", "^(e|err|error)$"]`

### 9. node/no-callback-literal
This rule enforces that a callback function is called with an `Error` object as the first parameter. In case there's no error, `null` or `undefined` are accepted as well.

```js
// ❌
cb('An error!');
callback(result);

// ✅
cb(new Error('An error!'));
callback(null, result);
```

This rule makes sure you don't accidentally invoke a callback function with a non-error as the first parameter. According to the [error-first callback convention](https://nodejs.org/en/knowledge/errors/what-are-the-error-conventions/), the first argument of a callback function should be the error or otherwise null or undefined if there's no error.

The rule is triggered only when the function is named `cb` or `callback`.

### 10. node/no-sync
This rule disallows using synchronous methods from the Node.js core API where an asynchronous alternative exists.

```js
// ❌
const file = fs.readFileSync(path);

// ✅
const file = await fs.readFile(path);
```

Using synchronous methods for I/O operations in Node.js [blocks the event loop](https://maximorlov.com/javascript-event-loop-talk/). In most web applications, you want to use asynchronous methods when doing I/O operations.

In some applications like a CLI utility or a script, using the synchronous method is okay. You can disable this rule at the top of the file with `/* eslint-disable node/no-sync */`.

## Additional rules for TypeScript users
If your project is using TypeScript you're probably familiar with [TypeScript ESLint](https://typescript-eslint.io/docs/linting/) (previously TSLint). The following rules are available to TypeScript projects only because they infer extra context from typing information.

### 11. @typescript-eslint/await-thenable
This rule disallows awaiting a function or value that is not a Promise.

```js
// ❌
function getValue() {
  return someValue;
}

await getValue();

// ✅
async function getValue() {
  return someValue;
}

await getValue();
```

While it's valid JavaScript to await a non-Promise value (it will resolve immediately), it's often an indication of a programmer error, such as forgetting to add parentheses to call a function that returns a Promise.

### 12. @typescript-eslint/no-floating-promises
This rule enforces Promises to have an error handler attached.

```js
// ❌
myPromise()
  .then(() => {});

// ✅
myPromise()
  .then(() => {})
  .catch(() => {});
```

This rule prevents floating Promises in your codebase. A floating Promise is a Promise that doesn't have any code to handle potential errors.

Always make sure to handle promise rejections otherwise your [Node.js server will crash](https://maximorlov.com/node-js-15-is-out-what-does-it-mean-for-you/#unhandled-rejections-are-thrown).

### 13. @typescript-eslint/no-misused-promises
This rule disallows passing a Promise to places that aren't designed to handle them, such as if-conditionals.

```js
// ❌
if (getUserFromDB()) {}

// ✅ 👎
if (await getUserFromDB()) {}

// ✅ 👍
const user = await getUserFromDB();
if (user) {}
```

This rule prevents you from forgetting to await an async function in a place where it would be easy to miss.

While the rule does allow awaiting inside an if-conditional, I recommend assigning the result to a variable and using the variable in the conditional for improved readability.

### 14. @typescript-eslint/promise-function-async
This rule enforces Promise-returning functions to be `async`.

```js
// ❌
function doSomething() {
  return somePromise;
}

// ✅
async function doSomething() {
  return somePromise;
}
```

A non-async function that returns a promise can be problematic since it can throw an Error object *and* return a rejected promise. Code is usually not written to handle both scenarios. This rule makes sure a function returns a rejected promise *or* throws an Error, but never both.

Additionally, it's much easier to navigate a codebase when you *know* that all functions that return a Promise and are therefore asynchronous, are marked as `async`.

## Enable these rules in your project right now
I've published an [ESLint configuration package](https://github.com/Maximization/eslint-config-async) that you can easily add to your project. It has all of the above rules enabled (except for the TypeScript ones).

You can install it with:

```shell
npm install --save-dev eslint eslint-config-async eslint-plugin-node
```

Then in your `.eslintrc` configuration file add the following:

```json
{
  "plugins": [
    "eslint-plugin-node"
  ],
  "extends": [
    "eslint-config-async"
  ]
}
```

That's it! Add these linting rules for asynchronous code to your project and fix any issues that show up. You might squash a bug or two! 🐛 🚫

## Master Asynchronous JavaScript 🚀

Learn how to write modern and easy-to-read asynchronous code with a **FREE 5-day email course**.

Through visual graphics you will learn how to decompose async code into individual parts and put them back together using a modern async/await approach. Moreover, with **30+ real-world exercises** you'll transform knowledge into a practical skill that will make you a better developer.

![Refactoring Callbacks, a FREE 5-day email course. 30+ real-world exercises, a visual guide and 5 days, 5 lessons.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ic1d6os14bjflg46fuyw.png)

👉 [**Get Lesson 1 now**](https://maximorlov.com/refactoring-callbacks-course/)