异步错误处理机制深度解析
一句话概括:JavaScript 异步错误处理的本质是「调用栈的时空错位」——try-catch 只能抓住同一微秒内发生在同一调用栈的错误,而 Promise 链式冒泡和 async/await 让异步错误重新变得可追踪。
核心知识点
1. try-catch 的同步边界
try-catch 的覆盖范围止步于当前调用栈。异步回调在未来的新调用栈中执行,已经走出了 try 块的控制范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 抓不到:回调跑的比 try 块活得久
try {
setTimeout(() => { throw new Error('boom'); }, 0);
} catch (e) {
console.log('永远不会到这里');
}
// ✅ 抓到:错误和 catch 在同一个调用栈
try {
throw new Error('boom');
} catch (e) {
console.log('抓到了:', e.message);
}
面试常问「那 forEach 里抛错能抓到吗?」——能,因为 forEach 的回调是同步执行的,还在同一个调用栈里。
2. Promise 错误冒泡链
Promise 内抛出的任何错误都会被自动包装成 rejection,沿着 .then() 链向下冒泡,直到撞上最近的 .catch()。
1
2
3
4
5
6
Promise.resolve()
.then(() => { throw new Error('链路错误'); })
.then(() => console.log('被跳过了'))
.then(() => console.log('也被跳过了'))
.catch(err => console.log('在这拦截:', err.message))
.then(() => console.log('catch 之后继续执行'));
关键细节:.catch() 返回的仍然是一个 Promise——处理完错误后链条可以继续。另外,构造函数里 throw 会自动变成 reject,不用手动调:
1
2
new Promise(() => { throw new Error('自动 reject'); })
.catch(e => console.log(e.message)); // '自动 reject'
3. async/await:让 try-catch 重获新生
await 会把异步操作「拉回」当前调用栈,所以 try-catch 又能用了——这是 async/await 最大的工程价值之一。
1
2
3
4
5
6
7
8
9
10
async function loadUser(id) {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('加载失败:', err.message);
return null; // 优雅降级
}
}
如果你偏好 Go 风格的 [err, data] 模式,封装一个工具函数也很简单:
1
2
3
4
5
const to = promise =>
promise.then(data => [null, data]).catch(err => [err, null]);
const [err, user] = await to(fetch('/api/user/1').then(r => r.json()));
if (err) return console.error(err);
4. unhandledrejection:全局兜底网
没有任何 .catch() 处理的 Promise rejection 不会让页面崩溃,但会在控制台静默报错。现代浏览器提供了 unhandledrejection 事件来兜底捕获:
1
2
3
4
5
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
// 上报到监控平台
event.preventDefault(); // 阻止控制台默认警告
});
这是前端监控 SDK(Sentry、Fundebug)的底层依赖——它们就是靠这个事件抓到你没 catch 的 Promise 错误。
5. 自定义错误类型:让错误自带身份
用 instanceof 区分错误类型,而不是靠字符串匹配 error.message:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NetworkError extends Error {
constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
class AuthError extends Error {
constructor(msg) { super(msg); this.name = 'AuthError'; }
}
async function request(url) {
try {
const res = await fetch(url);
if (res.status === 401) throw new AuthError('登录过期');
if (!res.ok) throw new NetworkError(`请求失败 ${res.status}`);
return res.json();
} catch (err) {
if (err instanceof AuthError) redirectToLogin();
else if (err instanceof NetworkError) showToast('网络异常,请重试');
else throw err; // 未知错误继续上抛
}
}
ES2022 还支持 Error.cause 链式关联原始错误:new Error('请求失败', { cause: originalError })。
其实你每天都在用
1. fetch 兜底 每次调接口你都下意识写上 .catch(() => showToast('网络异常')) 或 try { await fetch() } catch {}——这就是异步错误处理最朴素的应用。没写的话,接口挂了用户看到的就是白屏。
2. 按钮防连点 点击提交按钮→发请求→按钮 disabled →请求结束(成功或失败)→按钮恢复。这个流程靠的就是 finally 或者在 catch 里重置状态。如果忘了在失败路径重置,按钮就永远灰着。
3. React/Vue 的 Loading-Error-Data 三态 每个异步组件本质上都在处理三种状态。Error 态不处理,组件就崩了;React 的 Error Boundary 也是这个思路的组件化——用 componentDidCatch 把子树里的错误拦住,展示降级 UI。
4. 第三方 SDK 的 Promise 包装 微信 JSSDK、支付 SDK 的回调式 API,你 wrap 成 Promise 时如果忘了 reject 分支,调用方就永远卡在 pending 状态。写 new Promise((resolve, reject) => { wx.xxx({ success: resolve, fail: reject }) }) 时,fail 回调就是你的错误出口。
5. 路由守卫中的权限校验 router.beforeEach 里异步查登录态:失败了如果不 catch,整个路由跳转就卡死;catch 了重定向到登录页——用户体验瞬间从「页面不动了」变成「请先登录」。
常见误解
❌ 误区一:async 函数里不写 try-catch,错误会自动处理 async 函数返回的是 Promise,内部 throw 等价于 Promise.reject()。不写 catch,错误会沿调用链向上传播,直到触发 unhandledrejection。你看起来「没报错」,其实是没 await 它——错误悬在半空没人接。
1
2
async function boom() { throw new Error('💥'); }
boom(); // 不 await → 错误没人管 → 触发 unhandledrejection
❌ 误区二:.catch() 放 Promise 链开头就能保护整条链 Promise 错误的冒泡方向是向下,不是向上。.catch() 只能抓住它前面的 rejection,后面的 .then() 里抛错它管不着。
1
2
3
Promise.resolve()
.catch(() => console.log('这个 catch 啥也抓不到')) // 前面没有错误
.then(() => { throw new Error('后面的错'); }); // 这个没人抓
❌ 误区三:await promise 报错后,后面的代码还会继续执行 不会。await 遇到 rejection 会立即抛出,函数内后续代码全部跳过——跟同步 throw 行为一致。想要「出错也继续」,需要在 await 上包 try-catch 或用 .catch() 返回默认值。
❌ 误区四:.then(onFulfilled, onRejected) 和 .then(onFulfilled).catch(onRejected) 是一样的 不一样。前者只在当前 Promise reject 时触发 onRejected,后者能捕获整个链上所有错误(包括 onFulfilled 里抛出的)。面试官常拿这个挖坑:
1
2
3
4
5
6
7
8
9
10
11
// 方式 A:第二个参数
Promise.resolve()
.then(
() => { throw new Error('ouch'); },
err => console.log('A:', err) // ❌ 不会触发
);
// 方式 B:链式 .catch()
Promise.resolve()
.then(() => { throw new Error('ouch'); })
.catch(err => console.log('B:', err)); // ✅ 能抓到
一句话总结:异步错误不可怕,可怕的是你以为 try-catch 能 cover 一切。记住三个原则——异步回调只能内部 catch、Promise 用链式 catch 兜底、async/await 用 try-catch 回归同步直觉,剩下交给 unhandledrejection 做最后防线。