Frontend Development 8 min read

Recommended ESLint Rules for Writing Better Asynchronous JavaScript Code

This article introduces a set of ESLint rules that help developers write safer and more efficient asynchronous JavaScript by avoiding common pitfalls such as async promise executors, awaiting in loops, improper promise handling, and callback-related race conditions.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Recommended ESLint Rules for Writing Better Asynchronous JavaScript Code

Hello everyone, I'm ConardLi. Today I recommend several best practices for writing asynchronous JavaScript code, each accompanied by a corresponding ESLint rule that you can configure.

no-async-promise-executor

It is discouraged to pass an async function to the new Promise constructor because wrapping an async function in a Promise is usually unnecessary and can hide errors.

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

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

Using async inside the Promise constructor may prevent thrown exceptions from rejecting the promise.

no-await-in-loop

Awaiting inside a loop prevents concurrent execution and underutilizes JavaScript's event‑driven model.

// ❌
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);

no-promise-executor-return

Returning a value from a Promise executor is useless; instead resolve or reject with the appropriate value.

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

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

require-atomic-updates

Combining assignment with await can cause race conditions, leading to unpredictable values such as a total count that may be 3 or 5 instead of the expected 8.

// ❌
let totalPosts = 0;
async function addPosts(userId) {
  totalPosts += await getPosts(userId);
}
await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);

// ✅
let totalPosts = 0;
async function addPosts(userId) {
  const posts = await getPosts(userId);
  totalPosts += posts; // read‑modify‑write is atomic here
}
await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);

max-nested-callbacks

Avoid deep callback nesting; refactor callbacks into Promises and use async/await for clearer code.

/* 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);

no-return-await

When returning a Promise from an async function, you can omit the redundant await unless you need to catch errors with try/catch.

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

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

prefer-promise-reject-errors

Always reject Promises with an Error object to preserve stack traces.

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

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

node/handle-callback-err

In Node.js callbacks, always handle the first err argument to avoid uncaught errors.

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

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

node/no-sync

Avoid synchronous I/O in Node.js when asynchronous alternatives exist, as sync calls block the event loop.

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

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

@typescript-eslint/await-thenable

Do not await non‑Promise values; ensure the awaited expression returns a Promise.

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

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

@typescript-eslint/no-floating-promises

Always attach a .catch handler to Promises to handle rejections.

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

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

@typescript-eslint/no-misused-promises

Do not pass a Promise directly to places expecting a boolean or other non‑Promise value; await it first.

// ❌
if (getUserFromDB()) {}

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

Reference: https://maximorlov.com/linting-rules-for-asynchronous-code-in-javascript/

JavaScriptNode.jsBest PracticesasyncPromiseeslintlinting
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.