9月阅读周·JavaScript异步编程设计快速响应的网络应用:深入理解JavaScript事件,异步错误的处理篇

举报
叶一一 发表于 2024/09/25 10:10:04 2024/09/25
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》

当前阅读周书籍《JavaScript异步编程设计快速响应的网络应用》

异步错误的处理

像很多时髦的语言一样,JavaScript 也允许抛出异常,随后再用一个try/catch语句块捕获。如果抛出的异常未被捕获,大多数JavaScript环境都会提供一个有用的堆栈轨迹。举个例子,下面这段代码由于'{'为无效JSON对象而抛出异常。

EventModel/stackTrace.js

function JSONToObject(jsonStr) {
return JSON.parse(jsonStr);
}
var obj = JSONToObject('{');

回调内抛出的错误

如果从异步回调中抛出错误,会发生什么事?让我们先来做个测试。

EventModel/nestedErrors.js

setTimeout(function A() {
setTimeout(function B() {
setTimeout(function C() {
throw new Error('Something terrible has happened!');
}, 0);
}, 0);
}, 0);

上述应用的结果是一条极其简短的堆栈轨迹。

等等,A和B发生了什么事?为什么它们没有出现在堆栈轨迹中?这是因为运行C的时候,A和B并不在内存堆栈里。这3个函数都是从事件队列直接运行的。

基于同样的理由,利用 try/catch 语句块并不能捕获从异步回调中抛出的错误。下面进行演示。

EventModel/asyncTry.js

try {
setTimeout(function() {
throw new Error('Catch me if you can!');
}, 0);
} catch (e) {
console.error(e);
}

看到这里的问题了吗?这里的 try/catch 语句块只捕获 setTimeout函数自身内部发生的那些错误。因为 setTimeout 异步地运行其回调,所以即使延时设置为0,回调抛出的错误也会直接流向应用程序的未捕获异常处理器。

总的来说,取用异步回调的函数即使包装上 try/catch 语句块,也只是无用之举。(特例是,该异步函数确实是在同步地做某些事且容易出错。例如,Node的fs.watch(file,callback)就是这样一个函数,它在目标文件不存在时会抛出一个错误。)正因为此,Node.js中的回调几乎总是接受一个错误作为其首个参数,这样就允许回调自己来决定如何处理这个错误。举个例子,下面这个Node应用尝试异步地读取一个文件,还负责记录下任何错误(如“文件不存在”)。

EventModel/readFile.js

var fs = require('fs');
fs.readFile('fhgwgdz.txt', function(err, data) {
if (err) {
return console.error(err);
};
console.log(data.toString('utf8'));
});

客户端JavaScript库的一致性要稍微差些,不过最常见的模式是,针对成败这两种情形各规定一个单独的回调。jQuery的 Ajax方法就遵循了这个模式。

$.get('/data', {
success:successHandler,
failure:failureHandler
});

不管 API 形态像什么,始终要记住的是,只能在回调内部处理源于回调的异步错误。异步尤达大师会说:“做,或者不做,没有试试看一说。”

未捕获异常的处理

如果是从回调中抛出异常的,则由那个调用了回调的人负责捕获该异常。但如果异常从未被捕获,又会怎么样?这时,不同的 JavaScript环境有着不同的游戏规则……
1.在浏览器环境中

现代浏览器会在开发人员控制台显示那些未捕获的异常,接着返回事件队列。要想修改这种行为,可以给window.onerror附加一个处理器。如果windows.onerror处理器返回true,则能阻止浏览器的默认错误处理行为。

window.onerror = function(err) {
return true; //彻底忽略所有错误
};

在成品应用中,会考虑某种 JavaScript 错误处理服务,譬如Errorception。Errorception提供了一个现成的 windows.onerror 处理器,它向应用服务器报告所有未捕获的异常,接着应用服务器发送消息通知我们。

2.在Node.js环境中

在 Node环境中,window.onerror 的类似物就是 process 对象的uncaughtException事件。正常情况下,Node应用会因未捕获的异常而立即退出。但只要至少还有一个uncaughtException事件处理器,Node应用就会直接返回事件队列。

process.on('uncaughtException', function(err) {
console.error(err); //避免了关停的命运!
});

但是,自 Node 0.8.4起,uncaughtException 事件就被废弃了。据其文档

对异常处理而言,uncaughtException是一种非常粗暴的机制,它在将来可能会被放弃……

请勿使用uncaughtException,而应使用Domain对象。

Domain对象又是什么?你可能会这样问。Domain对象是事件化对象,它将 throw 转化为'error'事件。下面是一个例子。

EventModel/domainThrow.js

myDomain.run(function() {
setTimeout(function() {
throw new Error('Listen to me!')
}, 50);
});
myDomain.on('error', function(err) {
console.log('Error ignored!');
});

源于延时事件的throw只是简单地触发了Domain对象的错误处理器。

很奇妙,是不是?Domain对象让throw语句生动了很多。遗憾的是,仅在 Node 0.8+环境中才能使用 Domain 对象;在我写作本书时,Domain对象仍被视作试验性的特性。更多信息请参阅Node文档。

不管在浏览器端还是服务器端,全局的异常处理器都应被视作最后一根救命稻草。请仅在调试时才使用它。

抛出还是不抛出

遇到错误时,最简单的解决方法就是抛出这个错误。在Node代码中,大家会经常看到类似这样的回调:

function(err) {
if (err) throw err;
// ……
}

我们会经常沿用这一做法。但是,在成品应用中,允许例行的异常及致命的错误像踢皮球一样踢给全局处理器,这是不可接受的。回调中的throw相当于JavaScript写手在说“现在我还不想考虑这个”。

如果抛出那些自己知道肯定会被捕获的异常呢?这种做法同样凶险万分。2011年,Isaac Schlueter(npm的开发者,在任的Node开发负责人)就主张try/catch是一种“反模式”的方式。

try/catch 只是包装着漂亮花括弧的 goto语句。一旦跑去处理错误,就无法回到中断之处继续向下执行。更糟糕的是,通过throw语句的代码,完全不知道自己会跳到什么地方。返回错误码的时候,就相当于正在履行合约。抛出错误的时候,就好像在说,“我知道我正在和你说话,但我现在不想搭理你,我要先找你老板谈谈”,这太粗俗无礼了。如果不是什么紧急情况,请别这么做;如果确实是紧急情况,则应该直接崩溃掉。

Schlueter提倡完全将 throw 用作断言似的构造结构,作为一种挂起应用的方式——当应用在做完全没预料到的事时,即挂起应用。Node社区主要遵循这一建议,尽管这种情况可能会随着Domain对象的出现而改变。

那么,关于异步错误的处理,目前的最佳实践是什么呢?我认为应该听从 Schlueter 的建议:如果想让整个应用停止工作,请勇往直前地大胆使用throw。否则,请认真考虑一下应该如何处理错误。是想给用户显示一条出错消息吗?是想重试请求吗?还是想唱一曲“雏菊铃之歌”

总结

堆栈轨迹不仅告诉我们哪里抛出了错误,而且说明了最初出错的地方。在本文中,我们看到了为什么 throw 很少用作回调内错误处理的正确工具,还会了解如何设计异步API以绕开这一局限。


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。