DEV Community

Cover image for 14 Linting Rules To Help You Write Asynchronous Code in JavaScript
Maxim Orlov
Maxim Orlov

Posted on • Edited on • Originally published at maximorlov.com

14 Linting Rules To Help You Write Asynchronous Code in JavaScript

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. Enable them by adding them to your eslint configuration file.

1. no-async-promise-executor

This rule disallows passing an async function to the new Promise constructor.

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

// ✅
new Promise((resolve, reject) => {});
Enter fullscreen mode Exit fullscreen mode

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 instead, you could greatly improve the efficiency of your code.

// ❌
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);
Enter fullscreen mode Exit fullscreen mode

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.

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

// ✅
new Promise((resolve, reject) => {
  resolve(result);
});
Enter fullscreen mode Exit fullscreen mode

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?

// ❌
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);
Enter fullscreen mode Exit fullscreen mode

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.

// ✅
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);
Enter fullscreen mode Exit fullscreen mode

5. max-nested-callbacks

This rule enforces a maximum nesting depth for callbacks. In other words, this rule prevents callback hell!

/* 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);
Enter fullscreen mode Exit fullscreen mode

A deep level of nesting makes code hard to read and more difficult to maintain. Refactor callbacks to promises and use modern async/await syntax when writing asynchronous code JavaScript.

6. no-return-await

This rule disallows unnecessary return await.

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

// ✅
async () => {
  return getUser(userId);
}
Enter fullscreen mode Exit fullscreen mode

Awaiting a promise and immediately returning it is unnecessary since all values returned from an async function are wrapped in a promise. 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.

// 👎
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
  }
}
Enter fullscreen mode Exit fullscreen mode

7. prefer-promise-reject-errors

This rule enforces using an Error object when rejecting a Promise.

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

// ✅
Promise.reject(new Error('An error occurred'));
Enter fullscreen mode Exit fullscreen mode

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-n plugin. To use them, you need to install and add the plugin to the plugins array in your eslint configuration file.

8. node/handle-callback-err

This rule enforces error handling inside callbacks.

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

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

  console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

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: 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.

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

// ✅
cb(new Error('An error!'));
callback(null, result);
Enter fullscreen mode Exit fullscreen mode

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, 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.

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

// ✅
const file = await fs.readFile(path);
Enter fullscreen mode Exit fullscreen mode

Using synchronous methods for I/O operations in Node.js blocks the event loop. 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 (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.

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

await getValue();

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

await getValue();
Enter fullscreen mode Exit fullscreen mode

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.

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

// ✅
myPromise()
  .then(() => {})
  .catch(() => {});
Enter fullscreen mode Exit fullscreen mode

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.

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.

// ❌
if (getUserFromDB()) {}

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

// ✅ 👍
const user = await getUserFromDB();
if (user) {}
Enter fullscreen mode Exit fullscreen mode

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.

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

// ✅
async function doSomething() {
  return somePromise;
}
Enter fullscreen mode Exit fullscreen mode

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 that you can easily add to your project. It exports the base rules, Node.js specific rules, and TypeScript specific rules separately.

For non-Typescript users

Install the package and ESLint:

npm install --save-dev eslint eslint-config-async
Enter fullscreen mode Exit fullscreen mode

Then in your eslint.config.js configuration file add the following:

const asyncConfig = require("eslint-config-async");

module.exports = [
  ...asyncConfig.base, // enable base rules
  ...asyncConfig.node, // enable Node.js specific rules (recommended)
];
Enter fullscreen mode Exit fullscreen mode

For TypeScript users

Install the package and its peer dependencies:

npm install --save-dev eslint eslint-config-async typescript typescript-eslint
Enter fullscreen mode Exit fullscreen mode

In your eslint.config.js configuration file:

const tseslint = require("typescript-eslint");
const asyncConfig = require("eslint-config-async");

module.exports = [
  tseslint.configs.base, // adds the parser only, without any rules

  ...asyncConfig.base, // enable base rules
  ...asyncConfig.node, // enable Node.js specific rules (recommended)
  ...asyncConfig.typescript, // enable TypeScript specific rules
  {
    files: ["*.ts"], // tell ESLint to include TypeScript files
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: __dirname,
      },
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

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! 🐛 🚫

Transform Callbacks into Clean Async Code! 🚀

Tired of messy callback code? Download this FREE 5-step guide to master async/await and simplify your asynchronous code.

In just a few steps, you'll transform complex logic into readable, modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

Refactoring Callbacks Guide Preview

Get the FREE guide

Top comments (0)