先记住这个答案
try/catch 能接住两类错误:try 块里同步抛出的错误,以及被 await 的 Promise 变成 rejected 时抛出的错误。没有被 await 的 Promise、setTimeout 回调里抛的错,它都接不住。至于该不该接,看你接住后能不能做点有意义的事:接口失败能提示用户、能重试、能降级,就在调用处接;代码 bug 接住了也修不好,应该让它继续往上抛,由全局的 unhandledrejection 和监控系统收集。
try/catch只能接住被await的 Promise 拒绝- 漏写
await、定时器回调里的错误都接不住 - 能给用户交代的错误才就地接,bug 交给全局兜底
try里要写return await,否则拒绝会绕过catch
await 让拒绝变成了普通的 throw
await 一个 rejected 的 Promise 时,那一行会像 throw 一样抛出拒绝原因。所以异步错误也能用同步的写法接住。
function loadUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => (id > 0 ? resolve({ id }) : reject(new Error('用户不存在'))), 10)
})
}
async function show(id) {
try {
const user = await loadUser(id) // 拒绝时,这一行直接抛错
console.log('拿到用户', user.id)
} catch (err) {
console.log('提示用户:' + err.message) // 业务错误,就地给提示
}
}
show(1)
show(0)查看输出与解释
拿到用户 1
提示用户:用户不存在show(0) 里 loadUser 被拒绝,await 那一行抛出错误,后面的 console.log 不再执行,直接跳进 catch。这里接住是合理的:用户不存在是能预料的情况,给个提示就是完整的处理。
try/catch 接不住的几种写法
判断标准只有一个:错误发生的那一刻,代码是不是还停在这个 try 块里。只要 try 块已经执行完了,后面才出的错就和它没关系。
await 和回调里抛错JavaScriptasync function save() {
try {
loadUser(0) // 没写 await,拒绝发生时 try 早就执行完了
setTimeout(() => {
JSON.parse('{') // 这里抛的错也到不了下面的 catch
}, 0)
console.log('try 正常结束')
} catch (err) {
console.log('永远不会走到这里')
}
}
// 结果:打印「try 正常结束」,随后控制台出现未处理的拒绝和一个未捕获的错误loadUser(0) 没有 await,它的拒绝会变成「未处理的拒绝」,浏览器触发全局的 unhandledrejection 事件。setTimeout 的回调是在另一轮事件循环里执行的,那时 save 早就返回了,要接只能在回调内部自己写 try/catch。
return和return await在try里不一样- 在
try块里直接return loadUser(0),函数把这个 Promise 原样交出去,拒绝发生在函数外面,本地的catch不会执行。写成return await loadUser(0),拒绝在try里就抛出来了,catch才能生效。不在try块里时,两种写法效果一样,await可以省。
哪些错误不该在业务代码里接
TypeError: Cannot read properties of undefined 这类错误说明代码写错了。在业务函数里把它接住,只打一行日志然后继续跑,界面会停在一个半成功的状态,问题还被藏了起来。
比较稳的分层是这样:请求层统一处理网络错误和 401 这类通用状态,比如跳登录;页面里只接和当前操作有关的业务错误,比如「库存不足」弹个提示;剩下的全部往上抛,由 window.addEventListener('unhandledrejection', ...) 和 Sentry 统一上报。React 里渲染阶段的错误交给错误边界组件,但错误边界接不住事件处理和异步代码里的错误,这部分还是要靠 try/catch 或全局事件。
容易答错的地方
- 每个
await都包一层try/catch - 包得越多,越容易在某一层把错误吞掉。只在能做出处理的那一层接,其他层让错误自然冒泡。一个请求失败要提示,一个请求失败要回滚表单,这两种才值得分别接。
catch里打完日志就结束catch之后函数正常返回undefined,调用方会以为操作成功了。处理不了就在catch里重新throw err,或者返回一个明确的失败结果,比如{ ok: false, error }。- 以为
try/catch能接住所有异步错误 - 它只管被
await的那一个 Promise。比如先同时发出两个请求const a = loadA()和const b = loadB(),再先await a然后await b。如果b在a完成前就被拒绝,这时还没有人await它,浏览器会先报一次未处理的拒绝。并发请求要写成await Promise.all([loadA(), loadB()]),让一个await同时盯住两个。
面试官还会怎么问?
不想写 try/catch,还有别的写法吗?
可以直接在调用处接 .catch(),比如 const user = await loadUser(id).catch(() => null),失败时拿到 null 走降级。也有人封装成 [err, data] 的元组返回,类似 Go 的风格,适合一个函数里有多个互不相关的请求、各自要单独处理失败的场景。
finally 里适合写什么?
写不管成功失败都要做的收尾,比如关掉 loading、释放锁。注意别在 finally 里 return,它会覆盖 try 或 catch 里的返回值,连抛出的错误也会被吞掉。
全局 unhandledrejection 能代替 try/catch 吗?
不能。它只能在最后收集和上报,这时已经不知道是哪个按钮、哪个操作出的错,没法给用户具体提示,也没法回滚状态。它的定位是兜底和监控,业务错误还是要在调用处处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。