如何优雅的处理异步错误

原文:How to avoid uncaught async errors in Javascript

目录

  1. 异步 IIFE
  2. 异步 forEach
  3. Promise
  4. DOM 事件监听

一、异步 IIFE

一个简单的异步 IIFE

(async () => {
  throw new Error("err");
})();

先看看同步函数如何与try..catch块一起工作

try {
  (() => {
    throw new Error("err");
  })();
}catch(e) {
  console.log(e); // caught
}

将其更改为异步函数

try {
  (async () => {
    throw new Error("err"); 
  })();
}catch(e) {
  console.log(e) // uncaught
}

在这种情况下,catch 不会运行,这是为什么?

在同步情况下,错误是同步错误,因此同步的 try..catch 可以处理。

但是异步函数的工作方式不同,那里执行的同步操作会创建一个新的 Promise 并且滞后运行。

程序在抛出错误时离开,因此 try..catch 无法处理它。

解决方案

使用 catch 函数来处理错误

(async () => {
  throw new Error("err");
})().catch((e) => {
  console.log(e); // caught
});

或者在 async 函数中添加 try..catch

(async () => {
  try {
    throw new Error("err");
  }catch(e) {
    console.log(e); // caught
  }
})();

二、异步 forEach

用 try..catch 处理同步 forEach 中的错误

try{
  [1,2,3].forEach(() => {
    throw new Error("err");
  });
}catch(e) {
  console.log(e); // caught
}

但是若操作为异步就会出现异常

try{
  [1,2,3].forEach(async () => {
    throw new Error("err");
  });
}catch(e) {
  console.log(e)
}
// 结果:引发 3 个“未捕获”的异常
Uncaught (in promise) Error: err
Uncaught (in promise) Error: err
Uncaught (in promise) Error: err

可以使用 await Promise.all 解决:

try{
  await Promise.all([1,2,3].map(async () => {
    throw new Error("err");
  }));
}catch(e) {
  console.log(e); // caught
}

三、promise

简单的 primise 捕获异常

new Promise(() => {
  throw new Error("err");
}).catch((e) => {
  console.log(e); // caught
});
Promise.resolve().then(() => {
  throw new Error("err");
}).catch((e) => {
  console.log(e); // caught
})

但是如果 promise 内部有其他的异步处理将无法被捕获

new Promise(() => {
  setTimeout(() => {
    throw new Error("err"); // uncaught
  }, 0);
}).catch((e) => {
  console.log(e);
});

这个情况要用 reject 方式抛出异常才能被捕获

new Promise((res, rej) => {
  setTimeout(() => {
    rej('err') // caught
  }, )
}).catch((e) => {
  console.log(e)
})

另一种情况是多个 await 中报错执行的问题

const wait = (ms) => new Promise((res) => setTimeout(res, ms));
(async () => {
  try{
    const p1 = wait(3000).then(() => {throw new Error("err")}); // uncaught
    await wait(2000).then(() => {throw new Error("err2")}); // caught
    await p1;
  }catch(e) {
    console.log(e);
  }
})();

结果是只能捕获到 err2

Error: err2
Uncaught (in promise) Error: err

原因是 p1 等待 3s 后抛出异常,但 2s 后抛出了 err2 异常,中断了代码执行,

所以 await p1 不会被执行到,导致这个异常不会被 catch。

若当 p1 改成 1s 后

const wait = (ms) => new Promise((res) => setTimeout(res, ms));
(async () => {
  try{
    const p1 = wait(1000).then(() => {throw new Error("err")});
    await wait(2000);
    await p1;
  }catch(e) {
    console.log(e);
  }
})();

结果是 1s 后会抛出一个未捕获异常,

但再过 1s 这个未捕获异常就消失了,变成了捕获的异常!

这个行为相当奇怪,因此并行的 Promise 建议用 Promise.all 处理:

await Promise.all([
  wait(1000).then(() => {
    throw new Error('err')
  }),
  wait(2000),
])

这会处理这两个错误,即使只会抛出第一个错误。

另外 Promise 的错误会随着 Promise 链传递,

因此建议把 Promise 内多次异步行为改写为多条链的模式,在最后 catch 中错误。

new Promise((res, rej) => {
  setTimeout(() => {
    throw Error('err')
  }, 1000) 
}).catch((error) => {
  console.log(error)  // uncaught
})
// 改写成这样即可捕获
new Promise((res, rej) => {
  setTimeout(res, 1000) 
})
  .then((res, rej) => {
    throw Error('err')
  })
  .catch((error) => {
    console.log(error)  // caught
  })

原因是用 Promise 链式代替了内部多次异步嵌套,

这样多个异步行为会被拆解为对应 Promise 链式的同步行为,

Promise 就可以捕获啦。

四、DOM 事件监听

DOM 事件监听内抛出的错误都无法被捕获

document.querySelector("button").addEventListener("click", async () => {
  throw new Error("err"); // uncaught
});
document.querySelector("button").addEventListener("click", () => {
  throw new Error("err"); // uncaught
})
posted @ 2023-03-01 20:30  琪有此理  阅读(55)  评论(0编辑  收藏  举报