【前端面试小册】JS-第6节:Promise 与 async/await 异常处理

一、Promise 异常捕获的两种方式

在 JavaScript 异步编程中,正确处理 Promise 异常至关重要。主要有两种方式:.catch() 方法async/await + try/catch

方式一:使用 .catch() 捕获异常

function PromiseRejectTest() {
  return new Promise((resolve, reject) => {
    reject('抛出错误');
  });
}

async function test() {
  // ✅ 使用 .catch() 捕获 Promise 异常
  PromiseRejectTest()
    .catch(err => {
      console.log(err);  // 输出:抛出错误
    });
}

test();

执行流程

graph TD
    A[PromiseRejectTest 被调用] --> B[返回 rejected Promise]
    B --> C[.catch 捕获异常]
    C --> D[执行 catch 回调]
    D --> E[输出错误信息]

关键点

  • .catch() 会捕获 Promise 链中的任何错误
  • 即使没有 await.catch() 也能捕获 Promise 的 rejection

方式二:使用 await + try/catch 捕获异常

function PromiseRejectTest() {
  return new Promise((resolve, reject) => {
    reject('抛出错误');
  });
}

async function test() {
  try {
    // ⚠️ 必须使用 await,否则异常无法被捕获
    await PromiseRejectTest();
  } catch(e) {
    console.log('catch:', e);  // 输出:catch: 抛出错误
  }
}

test();

执行流程

graph TD
    A[test 函数执行] --> B[进入 try 块]
    B --> C[await PromiseRejectTest]
    C --> D[Promise rejected]
    D --> E[异常抛出到 catch 块]
    E --> F[执行 catch 处理]

关键点

  • 必须使用 await,否则 Promise 异常无法被 try/catch 捕获
  • await 会将 Promise 的 rejection 转换为可被 try/catch 捕获的异常

二、Promise 异常捕获的陷阱

陷阱一:未使用 await 的 try/catch 捕获不到

function PromiseRejectTest() {
  return new Promise((resolve, reject) => {
    reject('抛出错误');
  });
}

function test() {
  try {
    // ❌ 没有使用 await,异常无法被捕获
    PromiseRejectTest();
  } catch(e) {
    // ❌ 不会执行
    console.log('catch');
  }
}

test();

原因分析

graph TD
    A[调用 PromiseRejectTest] --> B[返回 Promise 对象]
    B --> C[Promise 状态变为 rejected]
    C --> D[异常是异步抛出的]
    D --> E[try 块已执行完毕]
    E --> F[catch 无法捕获异步异常]

关键理解

  • Promise 的 reject异步操作,不会同步抛出异常
  • try/catch 只能捕获同步异常await 后的异步异常
  • 没有 await 时,异常在 Promise 内部,无法被外部 try/catch 捕获

陷阱二:Promise 构造器中的 async 函数异常

// ❌ 错误示例:Promise 中直接使用 async 函数
new Promise(async (resolve) => {
  // 等价于 reject('Error')
  throw new Error('Error');
}).catch(e => {
  // ❌ 不会执行,异常无法被捕获
  console.log('catch');
});

原因分析

// 上面的代码等价于下面的结构
new Promise((resolve1, reject1) => {
  // async 函数返回一个新的 Promise
  return new Promise((resolve2, reject2) => {
    // 内层 Promise 抛出异常
    reject2('抛出错误');
    // 但异常并未向上传播给外层 Promise
  });
}).catch(e => {
  // 这里 catch 的是外层 Promise 的 reject1
  // 但是异常并未抛给外层 Promise,所以 catch 不到
  console.log('catch');  // ❌ 不会被执行
});

详细解释

graph TD
    A[new Promise 构造器] --> B[传入 async 函数作为 executor]
    B --> C[async 函数自动返回 Promise]
    C --> D[内层 Promise rejected]
    D --> E[异常在内层 Promise]
    E --> F[外层 Promise 无法捕获内层异常]
    F --> G[catch 无法执行]

关键理解

  • async 函数本身就是异步的,会返回一个新的 Promise
  • Promise 构造器的 executor 如果是 async 函数,相当于嵌套了两层 Promise
  • 内层 Promise 的异常不会自动向上传播到外层 Promise

正确的写法

// ✅ 方式一:在 async 函数内部处理异常
new Promise(async (resolve) => {
  try {
    throw new Error('Error');
  } catch(e) {
    reject(e);  // 手动将异常传递给外层 Promise
  }
}).catch(e => {
  console.log('catch:', e);  // ✅ 可以捕获
});

// ✅ 方式二:避免在 Promise 构造器中使用 async
new Promise((resolve, reject) => {
  // 直接同步抛出,会被 try/catch 捕获
  try {
    throw new Error('Error');
  } catch(e) {
    reject(e);  // 手动 reject
  }
}).catch(e => {
  console.log('catch:', e);  // ✅ 可以捕获
});

// ✅ 方式三:直接使用 async 函数
async function asyncFunc() {
  throw new Error('Error');
}

asyncFunc().catch(e => {
  console.log('catch:', e);  // ✅ 可以捕获
});

三、全局异常处理

浏览器环境

问题:浏览器中无法捕获未处理的 Promise rejection

// ❌ 这种方式异常会抛到全局,浏览器无法捕获
function test() {
  PromiseRejectTest();  // 未使用 await,也没有 .catch()
}

test();

解决方式:始终添加 .catch() 或使用 await

// ✅ 方式一:添加 .catch()
PromiseRejectTest().catch(err => {
  console.error('捕获到错误:', err);
});

// ✅ 方式二:使用 await + try/catch
async function test() {
  try {
    await PromiseRejectTest();
  } catch(err) {
    console.error('捕获到错误:', err);
  }
}

Node.js 环境

Node.js 提供了全局异常捕获机制:

// ✅ 捕获未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise rejection:', reason);
  // 可以在这里记录日志、发送错误报告等
});

// ✅ 捕获未捕获的异常
process.on('uncaughtException', (error) => {
  console.error('未捕获的异常:', error);
  // 注意:应该尽快退出进程
  process.exit(1);
});

// 示例
Promise.reject('未处理的错误');  // 会被 unhandledRejection 捕获

四、Promise 异常传播机制

异常传播规则

// Promise 链式调用中的异常传播
Promise.resolve()
  .then(() => {
    throw new Error('错误1');
  })
  .then(() => {
    console.log('不会执行');
  })
  .catch(err => {
    console.log('捕获到:', err.message);  // 输出:捕获到: 错误1
    throw new Error('错误2');
  })
  .catch(err => {
    console.log('捕获到:', err.message);  // 输出:捕获到: 错误2
  });

传播规则

  • Promise 链中的异常会向下传播,直到遇到 .catch()
  • .catch() 中抛出异常,会继续向下传播
  • 如果链中没有 .catch(),异常会变为未处理的 rejection

异常传播流程图

graph TD
    A[Promise 链开始] --> B[then 中抛出异常]
    B --> C{是否有 catch}
    C -->|有| D[catch 捕获异常]
    C -->|无| E[异常继续传播]
    E --> F{链中还有 catch}
    F -->|有| G[下一个 catch 捕获]
    F -->|无| H[未处理的 rejection]
    D --> I[catch 中可以处理或继续抛出]
    I --> J{是否继续抛出}
    J -->|是| E
    J -->|否| K[异常处理完毕]

五、最佳实践

1. 始终处理 Promise 异常

// ❌ 不好的做法:忽略异常
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// ✅ 好的做法:始终处理异常
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch(error) {
    console.error('获取数据失败:', error);
    // 返回默认值或重新抛出
    throw error;
  }
}

2. 避免在 Promise 构造器中使用 async

// ❌ 不好的做法
new Promise(async (resolve, reject) => {
  const data = await fetchData();
  resolve(data);
});

// ✅ 好的做法:直接使用 async 函数
async function getData() {
  const data = await fetchData();
  return data;
}

// 或者使用 Promise.resolve().then()
Promise.resolve()
  .then(async () => {
    const data = await fetchData();
    return data;
  });

3. 使用 Promise.all 处理多个异步操作

// ✅ 并发请求,统一处理异常
async function fetchMultipleData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);
    return { users, posts, comments };
  } catch(error) {
    console.error('获取数据失败:', error);
    // 如果任何一个请求失败,都会进入 catch
    throw error;
  }
}

4. 添加全局异常处理

// 在应用入口添加全局异常处理
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise rejection:', event.reason);
  // 可以在这里发送错误报告到监控平台
  event.preventDefault();  // 阻止默认的错误输出
});

// 在 Node.js 中
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise rejection:', reason);
});

六、面试要点总结

核心知识点

  1. 异常捕获方式

    • .catch() 方法:适用于 Promise 链
    • await + try/catch:必须在 async 函数中,且必须使用 await
  2. 常见陷阱

    • 未使用 awaittry/catch 无法捕获 Promise 异常
    • Promise 构造器中使用 async 函数会导致异常无法被外层捕获
  3. 异常传播

    • Promise 链中的异常向下传播
    • 未捕获的异常会变成未处理的 rejection

常见面试题

Q1: try/catch 能捕获 Promise 的异常吗?

答:需要分情况

  • ❌ 没有 await:无法捕获(因为异常是异步抛出的)
  • ✅ 有 await:可以捕获(await 会将 Promise rejection 转换为异常)

Q2: 为什么 Promise 构造器中使用 async 函数会捕获不到异常?

答:因为 async 函数返回新的 Promise,形成了嵌套结构。内层 Promise 的异常不会自动传播到外层 Promise,需要在内部手动处理或使用 try/catch

Q3: 如何避免未处理的 Promise rejection?

答:

  • 始终为 Promise 添加 .catch() 处理
  • 在 async 函数中使用 try/catch
  • 在应用入口添加全局异常监听(unhandledrejection 事件)

实战建议

  • ✅ 新代码优先使用 async/await + try/catch(更清晰)
  • ✅ 链式调用使用 .catch()(更简洁)
  • ✅ 避免在 Promise 构造器中使用 async 函数
  • ✅ 始终处理 Promise 异常,避免全局污染
#前端面试#
前端面试小册 文章被收录于专栏

每天3-4节,跟着我50天学完,一起上岸! 靠它,我拿下阿里/百度/滴滴/字节/快手/腾讯/银行等offer 上岸银行,作为面试官使用,跳槽复习使用的小册

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务