常见的异步模式与陷阱

深入分析 Promise 异步编程中的常见错误、回调地狱的成因、以及如何用正确的方式组织异步代码。


回调地狱

在 Promise 出现之前,异步编程的主流方式是回调函数。这导致了一种臭名昭著的反模式——回调地狱:

getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log('done:', d);
      });
    });
  });
});

回调地狱的问题:

  • 代码难以阅读,嵌套层级过深
  • 错误处理分散在每一层
  • 难以复用和测试

Promise 如何解决回调地狱

Promise 提供了链式调用,让异步代码可以写成扁平结构:

getData()
  .then(a => getMoreData(a))
  .then(b => getMoreData(b))
  .then(c => getMoreData(c))
  .then(d => console.log('done:', d))
  .catch(err => console.error(err));

关键改进:

  1. 扁平结构:不再嵌套
  2. 统一错误处理.catch() 捕获所有之前的错误
  3. 可复用:每个 .then() 可以是独立的函数

async/await 的最终形态

async/await 让我们能用同步风格写异步代码:

async function fetchAll() {
  try {
    const a = await getData();
    const b = await getMoreData(a);
    const c = await getMoreData(b);
    const d = await getMoreData(c);
    console.log('done:', d);
  } catch (err) {
    console.error(err);
  }
}

但这个写法有性能问题——每个 await 是串行的。如果它们之间没有依赖,应该并行执行:

async function fetchAllParallel() {
  const [a, b, c, d] = await Promise.all([
    getData(),
    getMoreData(null), // 不需要前一步的结果
    getMoreData(null),
    getMoreData(null)
  ]);
  console.log('done:', d);
}

常见陷阱

陷阱一:忘记 return Promise

// 错误:async2 不会等待 async1
async function wrong() {
  async function async1() { /* ... */ }
  async function async2() { /* ... */ }

  async1();
  async2();
}

// 正确
async function right() {
  await async1();
  await async2();
}

陷阱二:循环中的 await

// 错误:串行执行
async function wrong(urls) {
  const results = [];
  for (const url of urls) {
    const result = await fetch(url);
    results.push(result);
  }
  return results;
}

// 正确:并行执行
async function right(urls) {
  const results = await Promise.all(urls.map(url => fetch(url)));
  return results;
}

陷阱三:Promise.all 的部分失败

// 错误:一个失败全部失败
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);

// 正确:用 allSettled 处理部分失败
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successful = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

错误处理的最佳实践

始终处理 rejection

// 错误:unhandled rejection
someAsyncOperation()
  .then(result => process(result));

// 正确
someAsyncOperation()
  .then(result => process(result))
  .catch(error => console.error(error));

try/catch 在 async 函数中

async function fetchUser() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) throw new Error('HTTP error');
    return await response.json();
  } catch (err) {
    console.error('Fetch failed:', err);
    throw err;
  }
}

这一章想说的

Promise 和 async/await 大幅改善了 JavaScript 的异步编程体验,但引入了新的错误模式:

  1. 串行 vs 并行:没有依赖的异步操作应该并行执行
  2. 错误处理:每个 async 操作都应该处理 rejection
  3. 部分失败:用 Promise.allSettled 而不是 Promise.all 处理可能部分失败的情况

写好异步代码的关键是:理解 Promise 的执行模型,知道什么时候用 await,什么时候用 Promise.all


延展阅读