同步/异步异常捕获

在 JS 中,我们通常使用 try ... catch 来捕获异常。针对于捕获同步异常是比较容易的:

  1. try {
  2. throw new Error('err')
  3. } catch (e) {
  4. console.log(e) // caught
  5. }

但对于异步错误,捕获起来需要做额外的操作:

  1. try {
  2. ;(async () => {
  3. throw new Error('err') // uncaught
  4. })()
  5. } catch (e) {
  6. console.log(e)
  7. }

异常无法捕获的原因是异步代码并不在 try catch 上下文中执行。要捕获 async 函数中的异常,可以调用 .catch,因为 async 函数返回一个 Promise

  1. ;(async () => {
  2. throw new Error('err')
  3. }).catch((e) => console.log(e)) // caught

也可以在函数体内直接使用 try catch:

  1. ;(async () => {
  2. try {
  3. throw new Error('err')
  4. } catch (e) {
  5. console.log(e) // caught
  6. }
  7. })()

类似的,如果在函数体内部捕获异常,则要使用 Promise.all:

  1. ;(async () => {
  2. try {
  3. await Promise.all(
  4. [1, 2, 3].map(async () => {
  5. throw new Error('err')
  6. })
  7. )
  8. } catch (e) {
  9. console.log(e) // caught
  10. }
  11. })()

这里使用 await 等待异常返回,所以可以进行捕获。
若是 Promise 内再包含一个异步:

  1. new Promise(() => {
  2. setTimeout(() => {
  3. throw new Error('err') // uncaught
  4. }, 0)
  5. }).catch((e) => {
  6. console.log(e)
  7. })
  8. // 若想捕获,需要使用 reject 的方式进行抛出
  9. new Promise((resolve, reject) => {
  10. setTimeout(() => {
  11. reject('err') // caught
  12. }, 0)
  13. }).catch((e) => {
  14. console.log(e)
  15. })

并行的 Promise 建议使用 Promise.all 来进行处理:

  1. await Promise.all([
  2. wait(1000).then(() => {
  3. throw new Error('err')
  4. }), // p1
  5. wait(2000),
  6. ])

Promise 的错误会随着 Promise 进行链式传递,因此建议把 Promise 内多次的异步行为改写为多条链的方式,在最后使用 catch 来进行捕获。

  1. new Promise((res, rej) => {
  2. setTimeout(() => {
  3. throw Error('err')
  4. }, 1000) // 1
  5. }).catch((error) => {
  6. console.log(error)
  7. })
  8. // 将上面的例子改写为
  9. new Promise((res, rej) => {
  10. setTimeout(res, 1000) // 1
  11. })
  12. .then((res, rej) => {
  13. throw Error('err')
  14. })
  15. .catch((error) => {
  16. console.log(error)
  17. })

async 函数中直接抛出异常是无法被 try catch 捕获的

  1. try {
  2. (async () => {
  3. throw new Error('err')
  4. })()
  5. } catch (err) {
  6. console.log(err)
  7. }

此时我们需要将异常的抛出方式改为 Promise.reject

  1. (async () => {
  2. try {
  3. await (async () => {
  4. return Promise.reject(new Error('err'))
  5. })()
  6. } catch (err) {
  7. console.log(err)
  8. }
  9. })()

async 函数中

  1. async () => {
  2. return 1
  3. }
  4. // 等同于
  5. async () => {
  6. return Promise.resolve(1)
  7. }
  8. async () => {
  9. await 1
  10. // 或者单独的 1
  11. }
  12. // 等同于
  13. async () => {
  14. return Promise.resolve(1).then(() => undefined)
  15. }

全局捕获

仅通过 try catchthen 捕获同步、异步错误是不够的,这些都是局部的捕获手段,当我们无法保证所有代码都处理了异常的时候,需要进行全局异常监控,通常有以下两种方式:

  • window.addEventListener(‘error’)
  • widnow.addEventListener(‘unhandledrejection’)

error 可以监听所有同步、异步的运行时错误,但无法监听语法、接口、资源加载错误。而 unhandledrejection 可以监听到 Promise 中抛出的,未被 .catch 捕获的错误。
在具体的前端框架中,也可以通过框架提供的错误监听方案解决部分问题,比如 React 的 Error Boundaries、Vue 的 error handler,一个是 UI 组件级别的,一个是全局的。