问题描述
接口正常被调用且返回数据但前端页面渲染失败,控制台报错 Uncaught (in promise) error
Uncaught (in promise) error @ asyncToGenerator.js:6
Promise.then
getDeivceList @ index.vue:103
mounted @ index.vue:124
Promise.then
(匿名) @ permission.js:130
Promise.then
(匿名) @ permission.js:130
(匿名) @ permission.js:41
(匿名) @ permission.js:32
Promise.then
(匿名) @ permission.js:29
Promise.then
这些Promise错误就像调皮的小鬼,在调用栈里上蹿下跳。最气人的是,在我本地环境竟然能复现!
解决思路(TL;DR:)
经过排查,发现:
- 接口请求虽然返回了数据,但返回的code值为0,而非预期的200。导致项目中
响应拦截器
抛出了未捕获的Promise
错误。
导致产生这种情况的原因是:
- 后端同学未按照规范返回指定的响应码
- 前端同学在编写响应拦截器时未考虑更多的边界条件,未有错误预警。
破案之旅
先来一套"前端调试三板斧":
- 打断点追踪:从组件渲染层一路追踪到数据请求
- 查看网络响应:response里数据确实存在
- 数据类型校验:确定不是undefined在作妖
当检查到响应拦截器时,突然眼前一亮
// 响应拦截器
service.interceptors.response.use((res) => {// 未设置状态码则默认成功状态const code = res.data.code || 200;// 获取错误信息const msg = errorCode[code] || res.data.msg || errorCode["default"];// 二进制数据则直接返回if (res.request.responseType === "blob" ||res.request.responseType === "arraybuffer") {return res.data;}if (code === 401) {if (!isRelogin.show) {isRelogin.show = true;MessageBox.confirm("登录状态已过期,您可以继续留在该页面,或者重新登录","系统提示",{confirmButtonText: "重新登录",cancelButtonText: "取消",type: "warning",}).then(() => {isRelogin.show = false;store.dispatch("LogOut").then(() => {location.href = "/index";});}).catch(() => {isRelogin.show = false;});}return Promise.reject("无效的会话,或者会话已过期,请重新登录。");} else if (code === 500) {// Message({ message: msg, type: "error" });return Promise.reject(new Error(msg));} else if (code === 601) {Message({ message: msg, type: "warning" });return Promise.reject("error");} else if (code !== 200) {Notification.error({ title: msg });return Promise.reject("error");} else {return res.data;}
这里有个很魔性的判断:code !== 200
就会进错误处理。但你猜怎么着?后端同学在需求评审时说好的用200表示成功,结果上线时他们实际返回的code字段竟然是0!这就好比说好握手用右手,结果对方出左手。
于是当前端判断到code不是200时,直接触发reject:
// 原始代码判断逻辑
if (code !== 200) {Notification.error({ title: msg });return Promise.reject("error"); // 这里才是罪魁祸首
}
这导致调用链里的Promise异常没有被正确捕获,最终导致整个渲染流程崩溃。
后记
现在每次写拦截器都会留个后手:
我们给所有关键接口加了埋点监控,类似这样:
// 异常数据上报逻辑
if (unexpectedCode) {track({event: 'CODE_MISMATCH',payload: {expected: 200,actual: code,path: location.href}});
}
后记
现在每次对接新接口,我都会先确认这三个致命点:
- 状态码字段名叫code还是status?
- 正确的成功状态是数字类型还是字符串?
- 异常时的数据结构是否统一?
FAQ(来自团队内部提问)
Q:为什么不直接让后端改回200?
A:当时涉及多个正在测试中的下游系统,改动成本较高,故采用临时方案过渡
Q:Promise错误为何会导致整个页面崩溃?
A:由于我们在顶层没有写window.addEventListener(‘unhandledrejection’)事件监听
Q:怎么避免类似问题再次发生?
A:现在PR提交时会强制要求接口字段快照对比
👨💻 后来跟后端兄弟撸串时聊起这事,他说自己当时看错了文档里的状态码定义… 果然是血与泪的教训啊!