JavaScript错误处理指南
啊,JavaScript错误处理的危险。如果你相信墨菲定律任何可能出错的事情,都会出错。在本文中,我将探讨JavaScript中的错误处理。我将介绍一些陷阱、好的实践,最后以异步代码和Ajax结束。
这篇流行的文章于2017年6月8日更新,以解决读者的反馈。具体来说,将文件名添加到代码段中,清理单元测试,并添加包装器模式
uglyHandler
,增加了关于CORS和第三方错误处理程序的部分。
我觉得JavaScript的事件驱动范式增加了语言的丰富性。我喜欢把浏览器想象成事件驱动的机器,错误也不例外。当发生错误时,将在某个时刻抛出事件。理论上,错误是JavaScript中的简单事件。
如果这对你来说听起来很陌生,系好安全带,因为你将会有一段相当长的旅程。对于本文,我将只关注客户端JavaScript。
本主题基于中解释的概念JavaScript中的异常异常处理.如果你不熟悉的话,我建议你阅读一下基础知识。本文还假定您具有中级水平的JavaScript知识。如果你想要升级,为什么不注册SitePoint Premium并观看我们的课程呢必威西盟体育网页登录JavaScript:下一步.第一课是免费的。
在任何一种情况下,我的目标都是探索处理异常的基本必要条件之外的内容。阅读这篇文章会让你三思而后行,下次你看到一个漂亮的try…catch
块。
演示
我们将在本文中使用的演示可以在GitHub,并显示如下页面:
所有按钮点击时都会引爆一个“炸弹”。此炸弹模拟抛出的异常TypeError
.下面是该模块的定义:
/ /脚本/ error.js函数错误(){var喷火={};返回喷火.酒吧();}
首先,这个函数声明一个名为喷火
.请注意,酒吧()
哪里都找不到定义。让我们用一个好的单元测试来验证这是否会引爆炸弹:
/ /测试/脚本/ errorTest.js它('抛出TypeError',函数(){应该.抛出(错误,TypeError);});
单元测试已经开始了摩卡使用测试断言Should.js.Mocha是一个测试运行器,而Should.js是断言库。如果您还不熟悉这些测试api,请随意探索。测试开始于(描述)
以及格/不及格结束应该
.单元测试在Node上运行,不需要浏览器。我建议注意测试,因为它们用简单的JavaScript证明了关键概念。
克隆了repo并安装了依赖项之后,可以使用
npm t
.或者,你可以像这样运行这个单独的测试:。/ node_modules /摩卡/ bin / /脚本/ errorTest.js摩卡测试
.
如图所示,错误()
定义一个空对象,然后尝试访问一个方法。因为酒吧()
对象中不存在,则抛出异常。相信我,对于JavaScript这样的动态语言,每个人都会遇到这种情况!
坏
一些糟糕的错误处理。我从实现中抽象了按钮上的处理程序。下面是处理程序的样子:
/ /脚本/ badHandler.js函数badHandler(fn){试一试{返回fn();}抓(e){}返回零;}
此处理程序接收一个fn
Callback作为参数。然后在处理函数内部调用此回调。单元测试显示了它的用处:
/ /测试/脚本/ badHandlerTest.js它(“返回无错误的值”,函数(){varfn=函数(){返回1;};var结果=badHandler(fn);结果.应该.平等的(1);});它(“返回错误的空值”,函数(){varfn=函数(){扔新错误(随机误差的);};var结果=badHandler(fn);应该(结果).平等的(零);});
如您所见,这个糟糕的错误处理程序返回零
如果出了问题。回调fn ()
可以指向合法的方法或者炸弹。
下面的点击事件处理程序告诉了剩下的故事:
/ /脚本/ badHandlerDom.js(函数(处理程序,炸弹){varbadButton=文档.getElementById(“坏”);如果(badButton){badButton.addEventListener(“点击”,函数(){处理程序(炸弹);控制台.日志(“想象一下,因为隐瞒错误而获得晋升。”);});}}(badHandler,错误));
糟糕的是我只有一个零
.当我试图找出哪里出了问题时,这让我变得盲目。这种无故障策略的范围从糟糕的用户体验一直到数据损坏。令人沮丧的是,我可以花费数小时调试该症状,但却错过了try-catch块。这个邪恶的处理程序在代码中吞下错误,并假装一切正常。对于不注重代码质量的组织来说,这可能是可以接受的。但是,隐藏错误会让你在未来花费数小时进行调试。在具有深度调用堆栈的多层解决方案中,不可能找出哪里出了问题。至于错误处理,这是非常糟糕的。
无声失败策略会让你渴望更好的错误处理。JavaScript提供了一种更优雅的处理异常的方式。
丑陋的
是时候调查一个丑陋的处理程序了。我将跳过与DOM紧密耦合的部分。这里与你看到的糟糕的处理程序没有区别。
/ /脚本/ uglyHandler.js函数uglyHandler(fn){试一试{返回fn();}抓(e){扔新错误('一个新的错误');}}
重要的是它处理异常的方式,如下面的单元测试所示:
/ /测试/脚本/ uglyHandlerTest.js它('返回一个带有错误的新错误',函数(){varfn=函数(){扔新TypeError(类型错误的);};应该.抛出(函数(){uglyHandler(fn);},错误);});
比起糟糕的教练,这是一个明显的进步。在这里,异常通过调用堆栈被冒泡。我现在喜欢的是错误会Unwind堆栈这对调试非常有帮助。除了一个异常,解释器会沿着堆栈向上移动,寻找另一个处理程序。这为处理调用堆栈顶部的错误提供了许多机会。不幸的是,由于它是一个丑陋的处理程序,我失去了原始的错误。因此,我被迫遍历堆栈以找出原始异常。至少我知道出了什么问题,这就是为什么抛出异常的原因。
作为一种替代方法,可以使用自定义错误结束丑陋的处理程序。当你在错误中添加更多细节时,它不再是丑陋的,而是有用的。关键是要附加关于错误的特定信息。
例如:
/ /脚本/ specifiedError.js//创建自定义错误varSpecifiedError=函数SpecifiedError(消息){这.的名字=“SpecifiedError”;这.消息=消息||”;这.堆栈=(新错误()).堆栈;};SpecifiedError.原型=新错误();SpecifiedError.原型.构造函数=SpecifiedError;
/ /脚本/ uglyHandlerImproved.js函数uglyHandlerImproved(fn){试一试{返回fn();}抓(e){扔新SpecifiedError(e.消息);}}
/ /测试/脚本/ uglyHandlerImprovedTest.js它('返回带有错误的指定错误',函数(){varfn=函数(){扔新TypeError(类型错误的);};应该.抛出(函数(){uglyHandlerImproved(fn);},SpecifiedError);});
指定的错误将添加更多详细信息并保留原始错误消息。有了这个改进,它不再是一个丑陋的处理程序,而是干净和有用的。
使用这些处理程序,我仍然得到一个未处理的异常。让我们看看浏览器是否有办法解决这个问题。
Unwind堆栈
unwind异常的一种方法是放置try…catch
在调用堆栈的顶部。
例如:
函数主要(炸弹){试一试{炸弹();}抓(e){//处理所有错误}}
但是,记得我说过浏览器是事件驱动的吗?是的,JavaScript中的异常只不过是一个事件。解释器在执行上下文中停止执行并展开。事实证明,有一个Onerror全局事件处理程序我们可以用。
大概是这样的:
/ /脚本/ errorHandlerDom.js窗口.addEventListener(“错误”,函数(e){var错误=e.错误;控制台.日志(错误);});
此事件处理程序在任何执行上下文中捕获错误。任何类型的错误都会从不同的目标触发错误事件。如此激进的是这个事件处理程序将错误处理集中在代码中。与任何其他事件一样,您可以使用菊花链处理程序来处理特定的错误。这使得错误处理程序只有一个目的固体的原则。这些处理程序可以在任何时候注册。解释器将循环遍历所需的所有处理程序。释放代码库try…catch
块遍布,这使得调试很容易。关键是像对待JavaScript中的事件处理一样对待错误处理。
现在有了一种使用全局处理程序展开堆栈的方法,我们可以用它做什么呢?
毕竟,愿调用堆栈与您同在。
捕获堆栈
调用堆栈在故障排除方面非常有用。好消息是浏览器提供了开箱即用的信息。的堆栈财产不是标准的一部分,但在最新的浏览器上始终可用。
所以,例如,你现在可以在服务器上记录错误:
/ /脚本/ errorAjaxHandlerDom.js窗口.addEventListener(“错误”,函数(e){var堆栈=e.错误.堆栈;var消息=e.错误.toString();如果(堆栈){消息+ =' \ n '+堆栈;}varxhr=新XMLHttpRequest();xhr.开放(“职位”,“/日志”,真正的);//触发一个带有错误细节的Ajax请求xhr.发送(消息);});
从这个例子中可能不太明显,但这将与前面的例子一起启动。每个错误处理程序都有一个保存代码的目的干.
在浏览器中,事件处理程序获取附加到DOM。这意味着如果您正在构建第三方库,您的事件将与客户端代码共存。的window.addEventListener ()
为您解决这个问题,它不会掩盖现有的事件。
下面是这个日志在服务器上的截图:
这个日志存在于命令提示符中,是的,它毫无疑问地运行在Windows上。
此消息来自Firefox Developer Edition 54。使用适当的错误处理程序,请注意问题是什么是非常清楚的。不需要隐藏错误,通过瞥一眼这个,我可以看到是什么抛出异常以及在哪里抛出异常。这种级别的透明性有利于调试前端代码。您可以分析日志,了解哪些条件触发了哪些错误。
调用栈对调试很有帮助,千万不要低估调用栈的作用。
一个问题是,如果你有一个来自不同领域的脚本使歌珥您不会看到任何错误细节。例如,当您在CDN上放置脚本以利用每个域六个请求的限制时,就会发生这种情况。的e.message
只会说“脚本错误”,这是不好的。在JavaScript中,错误信息仅对单个域可用。
一种解决方案是重新抛出错误,同时保留错误消息:
试一试{返回fn();}抓(e){扔新错误(e.消息);}
一旦重新抛出错误,全局错误处理程序将完成剩下的工作。只需确保错误处理程序位于相同的域中。您甚至可以用特定的错误信息将其包装在自定义错误中。这将保留原始消息、堆栈和自定义错误对象。
异步处理
啊,异步的危险。JavaScript将异步代码从执行上下文中分离出来。这意味着像下面这样的异常处理程序有一个问题:
/ /脚本/ asyncHandler.js函数asyncHandler(fn){试一试{//从当前上下文中删除潜在的炸弹setTimeout(函数(){fn();},1);}抓(e){}}
单元测试会告诉你剩下的事情:
/ /测试/脚本/ asyncHandlerTest.js它(“不捕获带有错误的异常”,函数(){//炸弹varfn=函数(){扔新TypeError(类型错误的);};//检查异常是否被捕获应该.doesNotThrow(函数(){asyncHandler(fn);});});
异常没有被捕获,我可以用这个单元测试来验证这一点。注意,发生了一个未处理的异常,尽管我将代码包装在nicetry…catch
.是的,try…catch
语句只能在单个执行上下文中工作。异常抛出时,解释器已经从try…catch
.同样的行为也发生在Ajax调用中。
因此,一种替代方法是在异步回调中捕获异常:
setTimeout(函数(){试一试{fn();}抓(e){//处理这个异步错误}},1);
这种方法是可行的,但仍有很大的改进空间。首先,try…catch
街区到处都被缠在一起。事实上,20世纪70年代的糟糕编程打来电话,他们想要回他们的代码。另外,V8引擎不鼓励使用试着在函数中捕获块.V8是Chrome浏览器和Node中使用的JavaScript引擎。一种想法是将块移动到调用堆栈的顶部,但这对异步代码不起作用。
那么,这将把我们引向何方?我说全局错误处理程序在任何执行上下文中操作是有原因的。如果您向窗口对象添加错误处理程序,那么就完成了!保持干燥和坚固的决定正在得到回报,这很好。全局错误处理程序将使您的异步代码整洁。
下面是这个异常处理程序在服务器上报告的内容。注意,如果按照下面的步骤进行操作,所看到的输出将根据所使用的浏览器而有所不同。
这个处理程序甚至告诉我错误来自异步代码。它说它来自于setTimeout ()
函数。太酷了!
结论
在错误处理领域,至少有两种方法。一种是故障沉默方法,即忽略代码中的错误。另一种是快速失败和放松的方法,在这种方法中,错误会让世界停止并倒带。我认为我支持哪一个,以及为什么支持,这是很清楚的。我的观点是:不要隐藏问题。没有人会因为程序中可能发生的意外而羞辱你。停止,倒带,再给用户一次尝试是可以接受的。
在一个远非完美的世界里,允许第二次机会是很重要的。错误是不可避免的,重要的是你怎么做。
本文由蒂姆Severien而且莫里茨克罗格.感谢所有SitePoint的同行审必威西盟体育网页登录稿人,让SitePoint的内容成为最好的!