![]()
去年双十一,某电商平台的支付按钮在0点03分集体失灵。不是服务器挂了,是一个未捕获的undefined让前端脚本直接崩掉。用户看到的是转圈动画永远转下去,后台监控却安静得像深海。
这种「静默死亡」比报错更可怕。JavaScript的错误处理机制(Error Handling)本可以给开发者发警报,但很多人只会写业务代码,不会写「代码的保险丝」。
try...catch不是备胎,是主流程的一部分
新手常把try...catch当成补丁,出了问题才往上糊。但看这段真实踩坑代码:
// 某金融APP的净值查询接口 try { const netValue = await fetchFundData(fundCode); renderChart(netValue.history); } catch (e) { console.log('出错了'); }
问题在哪?catch块里只打了日志,用户看到的依然是空白图表区。更隐蔽的是,fetchFundData内部如果抛出非Error对象(比如reject了一个字符串),e.message会是undefined,后续的日志系统直接采集不到有效信息。
正确的姿势是把catch当成「降级服务」的触发器:
catch (error) { // 1. 结构化上报,带上下文 logger.error('FUND_FETCH_FAIL', { fundCode, errorType: error.constructor.name, stack: error.stack?.split('\n')[0] }); // 2. 用户侧兜底 renderChart(mockData.fallbackHistory); showToast('实时数据延迟,展示昨日净值'); }
graceful failure(优雅降级)的核心不是「不报错」,是「报错后用户无感知」。就像电梯坏了不会把人困在半空,而是自动平层开门。
finally是被遗忘的扫地机器人
很多人不知道finally的执行时机:无论try里return了还是catch里throw了,它都会跑。这个特性在资源清理场景是救命稻草。
看一个文件上传组件的内存泄漏案例:
![]()
// 错误示范:loading状态可能永远关不掉 async function upload(file) { setLoading(true); try { await api.upload(file); showSuccess(); } catch (e) { showError(e.message); return; // 这里直接return,loading永远true } setLoading(false); }
改成finally后:
async function upload(file) { setLoading(true); try { await api.upload(file); showSuccess(); } catch (e) { showError(e.message); } finally { setLoading(false); // 保证执行,像扫地机器人一定会回充 } }
更高级的用法是在finally里做「埋点收尾」。比如一个耗时操作的性能监控:
const start = performance.now(); try { await heavyComputation(); } finally { metrics.timing('heavy_op_duration', performance.now() - start); }
哪怕heavyComputation里抛错,埋点数据也能准确上报。这种「观测不中断」的能力,在排查线上问题时能省下数小时。
异步错误的3个陷阱,90%的人踩过
Promise和async/await让错误处理更隐蔽。这三个坑,简历上写「精通JS」的也未必能躲过。
陷阱1:回调里的try...catch是摆设
setTimeout(() => { throw new Error('boom'); }, 0); try { setTimeout(...); } catch (e) { // 抓不到!错误在另一个事件循环 }
异步回调的抛错会直接进入全局,触发window.onerror。正确做法是给回调内部包try...catch,或者把setTimeout改造成Promise再用.catch。
陷阱2:await链中的中间层吞错误
![]()
async function getUser() { const res = await fetch('/api/user'); return res.json(); // 如果res不是JSON,这里抛SyntaxError } // 调用方 try { await getUser(); } catch (e) { // 拿到的是SyntaxError,不知道是哪一步出的问题 }
解决方案是每层都加「错误装饰」,把上下文带下去:
if (!res.ok) throw new Error(`HTTP ${res.status} at /api/user`);
陷阱3:Promise.all的「一崩全崩」
Promise.all([fetchA(), fetchB(), fetchC()])里只要一个reject,整个就挂。但业务场景常需要「部分成功」——比如三个推荐位,能加载几个算几个。
这时候用Promise.allSettled,配合filter处理fulfilled的结果,是更务实的选择。
从「救火」到「防火」:错误处理的工程化
单个try...catch是手艺,全链路的错误治理才是工程。看两个被验证过的实践:
边界锚点(Error Boundary):React的错误边界机制,本质是在组件树的关键节点插try...catch。当子树崩溃,父级能渲染备用UI而不是白屏。这个思路可以迁移到任何框架——在路由级别、在微前端容器级别、在第三方SDK包裹层,都该有类似的「安全气囊」。
错误指纹(Fingerprinting):把error.stack用算法压缩成唯一ID,相同根因的错误聚类到一起。Sentry这类工具的核心能力就是这个。自研方案可以用stacktrace-parser提取函数名+行号,再哈希。
一个反直觉的数据:某大厂统计发现,线上JS错误的Top 3里,「Cannot read property of undefined」常年霸榜。这种错误本可以在编译期用TypeScript或严格空检查消灭,却跑进了生产环境。
工具能防的,别留给运行时。
回到开头那个双十一事故。事后复盘,根因是某次重构把try...catch里的兜底渲染逻辑删了,代码评审时没人注意到这个「负向变更」。现在的CI流程里,他们加了一条规则:任何删除catch块内代码的PR,必须附带测试用例证明降级路径仍可用。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.