异步函数嵌套调用的await使用疑问及最佳实践咨询
问题背景
我有一个webhook,调用异步函数A,A又调用包含异步数据库调用(C1、C2等)的异步函数B。webhook里用await调用A并搭配try/catch块,本想处理异常避免进程崩溃,但没生效:A提前返回,后续B或下层抛出未处理异常导致进程崩溃。后来发现是A内部没对B用await,加上后就能正常捕获异常了。
我刚接触JavaScript异步编程,有几个疑问:
- 若要完全等待函数调用完成,是否其下层所有异步调用都需加
await? - 使用第三方库时如何确保能等待其异步代码完成?
- 编写调用其他异步函数的异步函数时,是否需对可能让父级处理错误的异步调用都加
await? - 相关最佳实践有哪些?
复现代码
function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } async function sleepAndThrow() { await sleep(1000); throw new Error("Error from sleepAndThrow()"); } async function sleepAndThrowWrapperWithoutAwait() { sleepAndThrow(); } async function sleepAndThrowWrapperWithAwait() { await sleepAndThrow(); } async function main() { console.log("Running main()..."); try { // 换成sleepAndThrowWrapperWithAwait()就能正常捕获异常 await sleepAndThrowWrapperWithoutAwait(); console.log("Didn't expect to get here"); } catch (e) { console.error(e); } } main();
这段代码会意外执行console.log("Didn't expect to get here"),因为sleepAndThrowWrapperWithoutAwait里没await调用sleepAndThrow,导致外层的try/catch无法捕获后续抛出的异常。
解答
1. 是否下层所有异步调用都需要加await才能完全等待?
是的。只要你需要等待某个异步操作完成(包括它的所有后续异步逻辑),就必须对其使用await。
异步函数调用后会返回一个Promise对象,如果你不await它,JavaScript引擎会继续执行后续代码,而那个Promise会在后台运行。如果这个Promise后续抛出异常,因为没有被任何await或.catch()处理,就会变成未处理Promise拒绝,在Node.js中默认会导致进程崩溃,浏览器中会在控制台抛出错误。
就像你的例子里,sleepAndThrowWrapperWithoutAwait调用sleepAndThrow()但没加await,这个函数会立刻返回一个已解决的Promise(因为函数里没有等待异步操作),外层的await会直接继续执行后续代码,而sleepAndThrow里的异常会在1秒后抛出,此时已经脱离了外层的try/catch范围,导致未处理异常。
2. 使用第三方库时如何确保等待异步代码完成?
- 优先查文档:正规第三方库的异步API都会明确说明返回Promise,或者提供回调。如果是Promise返回的API,直接用
await即可;如果是回调风格的,可以用Node.js的util.promisify或者手动封装成Promise。 - 检查返回值:如果调用库的函数后得到一个Promise对象,就必须
await它才能等待异步操作完成。 - 避免“火并遗忘”:除非你明确不需要等待这个异步操作完成,也不关心它的结果和异常,否则一定要处理它的Promise(要么
await,要么加.catch())。
3. 编写异步函数时,是否需对所有要让父级处理错误的异步调用加await?
是的。如果希望父级的try/catch或者.catch()能捕获异步调用的异常,必须对这个异步调用使用await。
如果不await,异步调用的Promise会脱离当前函数的上下文,它的异常无法被当前函数的try/catch捕获,更无法传递到父级。这种情况下,要么手动给这个Promise加.catch()处理异常,要么就必须await它,把异常冒泡到上层。
最佳实践
- 绝不忽略异步操作的Promise:任何异步函数调用后,要么
await它,要么用.then()/.catch()处理,避免未处理的Promise拒绝。 - 始终在异步函数中等待内部异步操作:除非你明确要让这个操作在后台独立运行(且已经处理了它的异常),否则必须
await。 - 用
try/catch包裹await调用:在需要处理异常的层级,用try/catch捕获await抛出的错误,避免异常冒泡到顶层导致进程崩溃。 - 优先选Promise风格的第三方库API:如果库提供回调和Promise两种方式,优先选Promise,配合
await让代码更简洁易读。 - 监控未处理Promise拒绝:Node.js中可以通过
process.on('unhandledRejection', (err) => { ... })捕获未处理的Promise拒绝;浏览器中可以用window.addEventListener('unhandledrejection', ...),避免进程或页面意外崩溃。
内容的提问来源于stack exchange,提问作者rickjoi

