ESLint有一些规则可以帮助我们在写异步代码的时候减少错误、更符合规范,这里列出来14条 lint 规则,并说明了按照规则写异步代码和不按照规则写异步代码有何区别。最后还包含了几条在 NodeJS 中编写异步代码的规则

1. no-async-promise-executor

该规则禁止将 async函数传递给 new Promise 构造函数。

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

通常 new Promise 构造函数中返回 async 函数是没有问题的,并不会报错导致程序异常,但是有两个原因会让我们尽量避免这样使用:

  1. 如果async函数抛出错误,错误将会丢失,并且不会被新构造的 Promise 函数 reject
  2. 如果在 new Promise 中使用 async 函数,通常来说是没有必要的

2. no-await-in-loop

该规则禁止在循环内使用 await

通常在对一个可迭代对象中的每个元素执行操作并等待异步任务时,首先要想的是否有必要这么做?一般来说是不建议这么做的,使用 Promise.all 并行执行任务,可以极大的提高代码效率

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

当特殊情况下必须在 loop 中执行异步任务时,可以使用内联注释禁用掉该规则

// eslint-disable-line no-await-in-loop

3. no-promise-executor-return

该规则禁止在 Promise 构造函数中返回值

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

Promise 中返回的值不能被使用,也不会以任何方式影响 Promise。应该将该值传递给 resolve,或者发生错误时,调用 reject 并显示错误。

4. require-atomic-updates

该规则禁止将赋值与 await 结合使用,会导致竞态问题,类似于竞态请求

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

上面的代码 totalPosts 的最终值可能会与我们预期的答案不一致,不是 8,而是 5 或者 3.
原因是在读取和更新 totalPosts 之间存在时间差距。会导致竞争条件,当在单独的函数调用中更新值时,更新不会反应在当前函数作用域中,因此,两个函数都将其结果添加到totalPosts的初始值 0.正确的写法应该是确保在更新变量的同时读取它。

// ✅
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-nexted-callbacks

该规则限制回调函数的最大嵌套深度,用来防止回调地狱

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

6. no-return-await

该规则禁止不必要的 return await

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

等待 Promise 并立即返回是没有必要的,因为从 async 函数返回的所有值都包装在了 Promise 中,因此可以直接返回 Promise 函数。

有个特殊情况就是当函数被 try…catch 包裹时,删除 await 关键字会导致 reject 不会被捕获,在这种情况下,建议将结果赋值给另一行的变量,以明确意图

// 👎
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

该规则强制在Promise reject 时使用 Error 对象,好处是更容易地跟踪错误的来源,因为 Error 对象存在堆栈跟踪

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

以下规则是针对 NodeJS的,需要安装单独的 eslint 插件eslint-plugin-node来使用

8. node/handle-callback-err

该规则强制在回调函数内部进行错误处理。

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

在Node.js中,将错误作为第一个参数传递给回调函数是很常见的。忘记处理错误可能会导致应用程序的行为异常。

9. node/no-callback-literal

该规则强制使用 Error 对象作为第一个参数来调用回调函数,如果没有错误,也可以输入nullundefined

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

该规则确保不会意外的调用非错误作为第一个参数的回调函数,根据错误优先回调约定,回调函数的第一个参数应该是 Error,如果没有错误,则为 null 或者 undefined

10. node/no-sync

该规则禁止在存在异步方法的Node核心 API 中使用同步方法

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

在Node.js中对I/O操作使用同步方法会阻止事件循环。在大多数web应用程序中,您希望在进行I/O操作时使用异步方法。


以下是使用 TypeScript时的附加规则,需要配置 typescript-eslint 使用

11. @typescript-eslint/await-thenable

该规则禁止等待非 Promise的函数或值

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

12. @typescript-eslint/no-floating-promises

该规则强制 Promise 附加一个错误处理程序

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

13. @typescript-eslint/no-misused-promises

该规则静止将 Promise 传递给不是为处理它们而设计的地方,比如 if 条件

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

这条规则可防止忘记再容易错误的地方等待异步函数

14. @typescript-elsint/promise-function-async

该规则强制 Promise 返回函数为 async

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

返回 Promise的非async函数可能会有问题,因为它可能抛出一个 Error对象并返回一个被rejectPromise。通常不会编写代码来处理这两种情况。该规则确保函数返回 rejectPromise或抛出错误,但不会同时返回和抛出。


如何配置这些规则

安装:

npm install --save-dev eslint eslint-config-async eslint-plugin-node typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置:在 .eslintrc 配置文件中:

"plugins": [
  "eslint-plugin-node",
  "@typescript-eslint"
],
"extends": [
  "async",
  "async/node",
  "async/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
  "tsconfigRootDir": "__dirname",
  "project": ["./tsconfig.json"],
};

来源:14 Linting Rules To Help You Write Asynchronous Code in JavaScript - Maxim Orlov