多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

前端精读周刊:JavaScript 异步错误捕获全指南 —— 从 try/catch 到全局异常监控

前端精读周刊:JavaScript 异步错误捕获全指南 —— 从 try/catch 到全局异常监控 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载成熟的产品都有较高的稳定性要求仅前端就要做大量监控与错误上报。任何服务或代码都可能存在外部调用只要外部调用存在不确定性代码就可能出现异常因此捕获异常是每个前端工程师的基本功。本文基于前端精读周刊第 209 期对《How to avoid uncaught async errors in Javascript》的精读系统梳理 JavaScript 中同步、异步、Promise 链、事件回调等场景下的异常捕获方式并给出全局异常监控的完整落地方案读完即可掌握一套可复制的捕获所有异步 error实战方法论。为什么同步异常很容易捕获异步异常却失控捕获同步异常非常简单try { ;(() { throw new Error(err) })() } catch (e) { console.log(e) // caught }但异步错误却无法被直接捕获这不太直观try { ;(async () { throw new Error(err) // uncaught })() } catch (e) { console.log(e) }原因是异步代码并不在try catch上下文中执行。外层try catch包裹的同步逻辑只有一个——创建一个异步函数这个创建动作本身不会抛错而函数体内真正抛错的代码是在微任务Microtask队列里由事件循环推入调用栈执行的早已脱离try catch的作用域。关于异步代码只进入 Event Loop 队列、由宿主环境决定执行时机的运行机制可参见本仓库的 《精读《Javascript 事件循环与异步》》。捕获 async 函数内异常的三种基本手段手段一调用 .catchasync函数返回的是一个 Promise因此可以借助 Promise 的.catch捕获其内部的异常;(async () { throw new Error(err) })().catch((e) { console.log(e) // caught })手段二函数体内直接用 try catch;(async () { try { throw new Error(err) } catch (e) { console.log(e) // caught } })()手段三循环体里用 Promise.all类似的如果在循环体里捕获异常则要使用Promise.alltry { await Promise.all( [1, 2, 3].map(async () { throw new Error(err) }) ) } catch (e) { console.log(e) // caught }也就是说await修饰的 Promise 内抛出的异常可以被try catch捕获。这正是 async/await 语法糖的核心价值把回调地狱还原为看起来同步的代码从而让异常重新回到try catch的管辖范围内。写了 await 也捕获不到两类漏网之鱼漏网场景一Promise 内部再嵌套异步不是写了await就一定能捕获到异常。一种情况是 Promise 内再包含一个异步如setTimeout此时回调中的throw不再属于该 Promise 的执行上下文new Promise(() { setTimeout(() { throw new Error(err) // uncaught }, 0) }).catch((e) { console.log(e) })这个场景要用reject方式抛出异常才能被捕获new Promise((res, rej) { setTimeout(() { rej(err) // caught }, 0) }).catch((e) { console.log(e) })这与本仓库 《精读《JavaScript 错误堆栈处理》》 中在 macrotask 队列里reject行为可以被 catch 到而throw Error无法捕获的结论完全一致setTimeout中的throw无论如何都无法被外层 Promise 捕获而reject是 Promise 自己提供的机制天然可以被.catch接住。漏网场景二await 没有被执行到另一种情况是这个await根本没有被执行到const wait (ms) new Promise((res) setTimeout(res, ms)) ;(async () { try { const p1 wait(3000).then(() { throw new Error(err) }) // uncaught await wait(2000).then(() { throw new Error(err2) }) // caught await p1 } catch (e) { console.log(e) } })()p1等待 3s 后抛出异常但因为 2s 后抛出了err2异常中断了代码执行所以await p1不会被执行到导致这个异常不会被 catch 住。更奇怪的是如果换一个场景提前执行了p1等 1s 后再await p1那异常就从无法捕获变成可以捕获了const wait (ms) new Promise((res) setTimeout(res, ms)) ;(async () { try { const p1 wait(1000).then(() { throw new Error(err) }) await wait(2000) await p1 } catch (e) { console.log(e) } })()结论是浏览器 1s 后会抛出一个未捕获异常但再过 1s 这个未捕获异常就消失了变成了被catch捕获的异常。这是因为 Promise 的 rejection 在未被消费时会触发一次全局的未处理拒绝而一旦后续通过await p1相当于给该 Promise 挂上了处理函数引擎就会认为该拒绝已被处理于是这个幽灵异常从全局错误面板中消失。这个行为非常反直觉当程序复杂时很难排查因为并行的 Promise 建议统一用Promise.all处理await Promise.all([ wait(1000).then(() { throw new Error(err) }), // p1 wait(2000), ])这样所有并行任务要么全部成功要么在第一个失败时统一进入catch语义清晰且不会产生迟到的未捕获异常。Promise Chain把内部异步拆解为同步链Promise 的错误会随着 Promise 链传递因此建议把 Promise 内多次异步行为改写为多条链的模式在最后catch住错误。还是之前的例子Promise 无法捕获内部的异步错误new Promise((res, rej) { setTimeout(() { throw Error(err) }, 1000) // 1 }).catch((error) { console.log(error) })但如果写成 Promise Chain就可以捕获了new Promise((res, rej) { setTimeout(res, 1000) // 1 }) .then((res, rej) { throw Error(err) }) .catch((error) { console.log(error) })原因是用 Promise Chain 代替了内部多次异步嵌套这样多个异步行为会被拆解为对应 Promise Chain 的同步行为then回调中的throw属于该链的同步执行阶段Promise 就可以捕获了。事件监听回调中的异常只能靠函数体内 try catch最后DOM 事件监听内抛出的错误都无法被外部捕获document.querySelector(button).addEventListener(click, async () { throw new Error(err) // uncaught })同步也一样document.querySelector(button).addEventListener(click, () { throw new Error(err) // uncaught })原因与 Promise 嵌套异步如出一辙事件回调由事件循环在特定时机推入调用栈执行执行上下文与注册处的try catch完全隔离只能通过函数体内try catch来捕获document.querySelector(button).addEventListener(click, () { try { // 业务逻辑 throw new Error(err) } catch (e) { console.log(e) // caught } })全局异常监控兜住所有漏网的 error开篇提到要监控所有异常仅通过try catch、.then捕获同步、异步错误还是不够的因为这些是局部错误捕获手段。当我们无法保证所有代码都处理了异常时需要进行全局异常监控一般有两种方法window.addEventListener(error)window.addEventListener(unhandledrejection)两者的能力边界如下监听事件可捕获范围无法捕获的范围error所有同步、异步的运行时错误语法错误、接口错误、资源加载错误unhandledrejectionPromise 中抛出、且未被.catch捕获的错误同步 throw、已被处理的 rejection一个可落地的全局监控示例// 捕获同步、异步运行时错误 window.addEventListener(error, (event) { console.log(捕获运行时错误, event.message, event.filename, event.lineno) // 上报到监控平台 }) // 捕获未被 catch 的 Promise rejection window.addEventListener(unhandledrejection, (event) { console.log(捕获未处理的 Promise 拒绝, event.reason) // 上报到监控平台 })需要补充的是本仓库 《精读《JavaScript 错误堆栈处理》》 中还提到window.onerror方案它可以捕捉语法错误和运行时错误并拿到出错的信息、堆栈、出错的文件、行号、列号等但存在两个问题——全局监测会统计到浏览器插件中的 JS 异常且跨域时浏览器出于安全考虑会把异常内容隐藏为Script Error。对应的成熟解决方案是给应用内所需的script标签添加crossorigin属性并在 JS 所在的 CDN 服务器上添加Access-Control-Allow-Origin: *HTTP 头。框架层面的错误兜底在具体的前端框架中也可以通过框架提供的错误监听方案解决部分问题ReactError Boundaries组件级错误边界UI 组件级别的错误兜底Vueapp.config.errorHandler全局错误处理器。两者一个是 UI 组件级别的一个是全局的可配合上面的原生全局监听一起使用形成局部 try catch 框架兜底 全局监控三层防线。精读总结await 链的一条链原则回头看JS 本身提供的try catch错误捕获是非常有效的之所以会遇到无法捕获错误的情况大多是因为异步导致的。然而大部分异步错误都可以通过await的方式解决。唯一要注意的是await仅支持一层或者说一条链的错误监听。比如这个例子是可以监听到错误的try { await func1() } catch (err) { // caught } async function func1() { await func2() } async function func2() { throw Error(error) }也就是说只要这一条链内都被await住了那么最外层的try catch就能捕获异步错误。但如果有一层异步又脱离了await那么就无法捕获了async function func2() { setTimeout(() { throw Error(error) // uncaught }) }针对这个问题本仓库 《精读《async await 是把双刃剑》》 也有过提醒不要随意 await、不要滥用 async/await 语法糖并行任务应先用Promise.all统一管理。原文也提供了Promise.all、链式 Promise、.catch等方法解决异步错误捕获问题因此只要编写代码时注意对异步的处理就可以用try catch捕获这些异步错误。一张表记住所有场景的捕获策略代码形态能否被外层捕获推荐捕获方式同步throw能try catchasync函数内throw能.catch或函数体内try catchawait的 Promise 抛错能外层try catch循环内并行异步抛错能await Promise.all(...)try catchPromise 内部setTimeout中throw不能改用reject或改写成 Promise Chain并行 Promise 提前抛错、后续才await视时机统一用Promise.all避免幽灵异常DOM 事件回调中throw不能函数体内try catch所有漏网错误全局window.addEventListener(error)unhandledrejection本文所依据的原始精读文档为 《精读《捕获所有异步 error》》属于本仓库 readme 中前沿技术系列的第 209 期同系列还收录了 《精读《JavaScript 错误堆栈处理》》、《精读《Javascript 事件循环与异步》》 与 《精读《async await 是把双刃剑》》可组合阅读形成对 JS 异步与错误处理的完整认知。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Teraterm终极指南从入门到精通的终端仿真神器完全教程Teraterm终极指南从入门到精通的终端仿真神器完全教程 Tera Term是一款功能强大的免费开源终端仿真器软件专为Windows系统设计支持SSH、桌面应用通信网络一键获取智慧教育资源免费电子课本下载工具终极指南一键获取智慧教育资源免费电子课本下载工具终极指南 在数字化教育时代 国家中小学智慧教育平台 为全国师生提供了丰富的电子教材资源。然而如何高效地获取这些宝贵网页爬虫教育终极Flutter异步错误处理指南从try/catch到全局捕获的实战技巧终极Flutter异步错误处理指南从try/catch到全局捕获的实战技巧 Kazumi是一款基于自定义规则的番剧采集APP支持流媒体在线观看和弹幕功能。在移动开发桌面应用音视频上一篇OpenSpeedy资源使用API性能数据采集接口下一篇开发者必看PayPal-Python-SDK核心组件与API调用详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表