先记住这个答案
async 函数里的同步 throw 不会冒泡给调用方。引擎会把整个函数体包在一层「捕获」里,函数体一旦抛错,返回的 Promise 就变成 rejected,错误对象就是拒绝原因。调用本身永远正常返回一个 Promise,所以要么 await 它再用 try/catch,要么在后面接 .catch(),否则这个错误就成了未处理的拒绝。
- async 函数不会同步抛错,错误都变成 rejected Promise
- 调用时不
await,外层try/catch接不住 - 参数默认值里出的错也会变成 rejected
setTimeout回调里抛的错不归这个 Promise 管
先看代码和输出
下面这个函数第一行就抛错,看看调用方什么时候、以什么形式拿到它。
throw 的 async 函数JavaScriptasync function load() {
console.log('进入函数')
throw new Error('参数不合法') // 同步抛错
}
const p = load() // 这一行不会抛异常
console.log('拿到 Promise', p instanceof Promise)
p.catch((err) => console.log('catch 到', err.message))
console.log('同步代码结束')查看输出与解释
进入函数
拿到 Promise true
同步代码结束
catch 到 参数不合法进入函数 是同步打印的,说明函数体确实立刻执行了。但 throw 之后调用方没有崩,而是拿到了一个 Promise。.catch() 的回调是微任务,要等同步代码跑完才执行,所以 catch 到 排在最后。
你可以把 async 函数想成自带一个 try/catch 的包装:try 里跑函数体,catch 里调用 reject(err)。这层包装连参数默认值都罩住了,比如 async function f(a = b.c) {} 在 b 是 undefined 时,也是返回 rejected Promise,而不是同步报 TypeError。
为什么外层 try/catch 接不住
同步的 try/catch 只能接住「这一行执行过程中抛出来」的异常。调用 async 函数这一行从来不抛异常,它只是返回了一个会失败的 Promise。
await 和 await 的对比JavaScriptasync function load() {
throw new Error('boom')
}
let p
try {
p = load() // 没有 await,这里不抛
console.log('try 里一切正常')
} catch (err) {
console.log('同步 catch', err.message) // 不会执行
}
p.catch((err) => console.log('只有 .catch 接得住', err.message))
async function main() {
try {
await load() // await 把 rejected 转成这一行的异常
} catch (err) {
console.log('await 后 catch', err.message)
}
}
main()查看输出与解释
try 里一切正常
只有 .catch 接得住 boom
await 后 catch boomawait 的作用是:Promise 成功就取出值,失败就在当前这一行把拒绝原因重新抛出来。所以 try/catch 必须和 await 配合,缺了 await 的话,try 只包住了一次成功的函数调用。
- 和普通函数返回 Promise 的区别
- 普通函数
function load() { if (!id) throw new Error('缺 id'); return request(id) }就不一样了。return之前的throw会同步抛给调用方,而request里的失败走 Promise。调用方要同时写try/catch和.catch()才能兜住两种错误。把它改成async function,两种错误就统一成 rejected Promise,这是很多团队要求「返回 Promise 的函数一律写成 async」的原因。
容易答错的地方
- 以为 async 函数里所有错误都会进 Promise
- 只有函数体同步执行的部分和
await链上的错误才会。在函数里写setTimeout(() => { throw err }),回调是在另一个宏任务里执行的,那时 async 函数早就返回了,这个错误会变成全局的未捕获异常,返回的 Promise 根本不知道。要让它进 Promise,得先把setTimeout包成 Promise 再await。 - 以为「同步抛错」时后面的调用方代码不会执行
- 普通函数同步抛错,调用处后面的代码确实不会执行。但 async 函数不是这样,调用处后面的同步代码照常跑完,错误要等到微任务阶段才被
.catch()处理。示例里同步代码结束先于catch 到打印就是这个原因。 - 把
return promise写在try里以为能接住 - 在 async 函数里写
try { return request() } catch {}时,request()的拒绝不会进这个catch,因为return直接把 Promise 交出去了,函数已经离开了try。要接住得写成return await request()。
面试官还会怎么问?
没人处理的 rejected Promise 会怎样?
浏览器会在控制台打印 Uncaught (in promise),并在 window 上触发 unhandledrejection 事件,可以在这里统一上报。Node.js 从 15 开始默认会因为未处理的拒绝直接让进程退出,所以服务端代码里漏写 .catch() 后果更严重。
new Promise 的执行器里 throw 会怎样?
和 async 函数一样,执行器里的同步 throw 会被捕获,Promise 变成 rejected。但执行器里异步回调抛的错同样接不住,只能在回调里自己调用 reject。
怎么让 async 函数的参数校验同步报错?
把校验放到外面一层普通函数里:先同步检查参数,不合法就直接 throw,合法再调用内部的 async 函数并返回它的 Promise。这样调用方写错参数时立刻就能在调用栈上看到错误,不用等到微任务阶段。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。