Node.js中的異常處理

記得剛剛開始學(xué)Node.js時(shí)自己嘗試著寫了一個(gè)簡(jiǎn)單的http服務(wù)器遥巴,跟以前接觸過的php相比感覺更自由姻锁,編起碼來也更爽了肮雨。但是某天發(fā)現(xiàn)稍微一個(gè)很小的錯(cuò)誤就導(dǎo)致整個(gè)http進(jìn)程掛掉了体斩,頓時(shí)有種不靠譜的感覺啊梭稚,跟php比起來感覺Node.js容錯(cuò)能力確實(shí)弱了很多,起碼一個(gè)php文件出錯(cuò)也不會(huì)導(dǎo)致所有的服務(wù)都掛掉絮吵。

后來接觸到Node.js web開發(fā)框架后感覺也不是那么輕易就讓整個(gè)進(jìn)程都掛掉的哨毁,于是便想研究下Node.js究竟是如何來處理各種異常從而避免整個(gè)進(jìn)程掛掉的。

當(dāng)我們的程序運(yùn)行在Node.js進(jìn)程里不小心拋出一個(gè)異常時(shí)便會(huì)觸發(fā)process對(duì)象的_fatalException方法源武,并將異常對(duì)象err傳進(jìn)去扼褪,_fatalException方法主要做以下一些處理:

當(dāng)process對(duì)象上有綁定domain時(shí)便調(diào)用domain對(duì)象的_errorHandler方法來處理,

if (process.domain && process.domain._errorHandler)
        caught = process.domain._errorHandler(er)

_errorHandler會(huì)返回一個(gè)布爾值來通知當(dāng)前程序domain是否有對(duì)該異常進(jìn)行處理粱栖,如果domain沒有做處理话浇,此時(shí)process對(duì)象便會(huì)觸發(fā)一個(gè)綁定到process上的uncaughtException事件來處理該異常,并且同樣會(huì)返回一個(gè)布爾值來通知當(dāng)前程序是否有對(duì)異常進(jìn)行處理闹究。

if (!caught)
        caught = process.emit('uncaughtException', er);

走到這個(gè)地步時(shí)如果異常還沒被正常的處理那么此時(shí)process就有點(diǎn)不高興了幔崖,既然你們都不處理那我就準(zhǔn)備讓你們?nèi)繏斓舭桑?確實(shí)太狠了點(diǎn)啊),這個(gè)時(shí)候悲劇即將發(fā)生渣淤。赏寇。。

if (!caught) {
        try {
          if (!process._exiting) {
            process._exiting = true;
            process.emit('exit', 1);
          }
        } catch (er) {
        }
}

如果異常都被妥妥的處理掉了那么Node.js進(jìn)程便會(huì)處理當(dāng)前事件的收尾的工作价认,比如調(diào)用process.nextTick傳進(jìn)去的回調(diào)函數(shù)在這個(gè)時(shí)候就準(zhǔn)備被調(diào)用了嗅定,然后繼續(xù)執(zhí)行事件隊(duì)列里的下一個(gè)事件

t = setImmediate(process._tickCallback)

總結(jié)下來Node.js中異常處理流程大概就是這樣的:

nodejs
nodejs

這整個(gè)過程中有個(gè)很重要的處理環(huán)節(jié)沒有加上去,那就是上面提到的domain對(duì)象用踩。
首先簡(jiǎn)單介紹下domain對(duì)象的使用場(chǎng)景以及基本使用方法:

當(dāng)我們開啟一個(gè)Node.js的http服務(wù)器時(shí)不可避免的會(huì)出現(xiàn)各種我們沒有預(yù)期到的異常渠退,并且我們預(yù)先寫好的try catch也無法捕捉。這時(shí)最關(guān)鍵的是如何保證整個(gè)服務(wù)進(jìn)程不會(huì)掛掉脐彩,并且能夠很友好的反饋給瀏覽器端的用戶碎乃。盡管process對(duì)象提供了一個(gè)uncaughtException事件方法讓我們可以處理異常并且保證當(dāng)前的服務(wù)進(jìn)程不會(huì)掛掉,但由于丟失了當(dāng)前的上下文惠奸,說得直接點(diǎn)就是丟失了response對(duì)象很難向用戶及時(shí)并且友好的輸出錯(cuò)誤提示梅誓,此時(shí)便陷入了用戶會(huì)一直傻傻的等待服務(wù)器超時(shí)(早就關(guān)閉網(wǎng)站了)的尷尬場(chǎng)景。

有了domain模塊我們便可以很方便的處理上面描述的場(chǎng)景了,剛剛開始接觸domain這個(gè)模塊時(shí)真不知道是個(gè)啥東西梗掰,名字都叫的怪怪的删豺。后來去翻了先官網(wǎng)上有關(guān)domain的文檔才知道這貨到底有啥作用,我們就依照官網(wǎng)的示例來說明domain如何處理上述場(chǎng)景:

http.createServer(function(req, res) {
    var reqd = domain.create();
    reqd.add(req);
    reqd.add(res);
    reqd.on('error', function(er) {
      console.error('Error', er, req.url);
      try {
        res.writeHead(500);
        res.end('Error occurred, sorry.');
      } catch (er) {
        res.end('Error sending 500', er.message, req.url);
      }
 });

當(dāng)res對(duì)象調(diào)用各種方法產(chǎn)生異常時(shí)愧怜,之前創(chuàng)建好的domain對(duì)象reqd便會(huì)收到通知,從而觸發(fā)我們預(yù)先設(shè)置好的處理方法來即使并且友好的輸出給用戶妈拌,避免超時(shí)這種糟糕的用戶體驗(yàn)拥坛!對(duì)于domain對(duì)象其他的方法大家可以直接翻看Node.js官網(wǎng)文檔的介紹,我這里就不啰嗦了~

下面我們著重的來研究下domain對(duì)象為何如此神奇尘分?
當(dāng)我們r(jià)equire('domain')對(duì)象時(shí)便對(duì)event模塊的EventEmitter對(duì)象產(chǎn)生了影響
EventEmitter.usingDomains = true;
緊跟著對(duì)process的domain屬性進(jìn)行了覆蓋

Object.defineProperty(process, 'domain', {
  enumerable: true,
  get: function() {
    return _domain[0];
  },
  set: function(arg) {
    return _domain[0] = arg;
  }
});

domain模塊本身維護(hù)著一個(gè)存放domain對(duì)象的數(shù)組 _domain猜惋,再接著就是告訴process對(duì)象要使用到domain了
process._setupDomainUse(_domain, _domain_flag);
調(diào)用這個(gè)方法后影響到的地方可不少,之前我們說過Node.js每個(gè)事件都會(huì)調(diào)用一下_tickCallback來處理之前調(diào)用process.nextTick保存到事件隊(duì)列里的回調(diào)函數(shù)培愁,現(xiàn)在Node.js不調(diào)用了這個(gè)了著摔,換成了調(diào)用_tickDomainCallback方法來代替_tickCallback。繼續(xù)我們的domain模塊定续,當(dāng)創(chuàng)建一個(gè)新的domain對(duì)象時(shí)便初始化了的它的members屬性來存放該domain要守護(hù)的對(duì)象谍咆,對(duì)照著上述的代碼

var reqd = domain.create();

此時(shí)reqd.members=[], 于是我們調(diào)用add方法將req已經(jīng)res對(duì)象都添加到domain中,由domain來幫他們處理各種錯(cuò)誤私股。
reqd.add(req);
reqd.add(res);
接著告訴domain當(dāng)req或者res操作出異常時(shí)應(yīng)該如何處理

reqd.on('error', function(er) {
      console.error('Error', er, req.url);
      try {
        res.writeHead(500);
        res.end('Error occurred, sorry.');
      } catch (er) {
        console.error('Error sending 500', er, req.url);
      }
});

其實(shí)就上面那樣還是沒法捕獲到異常摹察,甚至都無法響應(yīng),因?yàn)槲覀冞€沒調(diào)用res.write或者res.end方法來向用戶輸出內(nèi)容倡鲸,就算我們加上

reqd.add(req);
reqd.add(res);
res.test('end');

依然無法像我們預(yù)期想象的那樣進(jìn)入異常處理回調(diào)方法里供嚎,別忘了將可能發(fā)生異常的代碼放入domain.run中來執(zhí)行,就像這樣的:

var domain=require('domain'),
    http=require('http');

http.createServer(function(req, res) {
    var reqd = domain.create();
    reqd.add(req);
    reqd.add(res);
    reqd.on('error', function(er) {
      console.error('Error', er, req.url);
      try {
        res.writeHead(500);
        res.end('Error occurred, sorry.'+ er.message);
      } catch (er) {
        console.error('Error sending 500', er, req.url);
      }
    });

    reqd.run(function(){
        res.test();
        res.end('end');
    });
}).listen(1337);

此時(shí)一切都已就緒峭状,萬事俱備只欠東風(fēng)了克滴,就等著各種異常來臨了。ok, 此時(shí)由于res的某個(gè)操作(比如調(diào)用不存在的test方法)導(dǎo)致了一個(gè)異常的產(chǎn)生优床。根據(jù)最開始描述的處理流程劝赔,這個(gè)異常會(huì)被Node.js進(jìn)程傳到process._fatalException中進(jìn)行處理,如果process上綁定有domain對(duì)象則會(huì)調(diào)用domain的_errorHandler方法來處理異常胆敞,那_errorHandler究竟如火如荼處理異常的呢望忆?在討論這個(gè)問題之前我們先回到上面的reqd.run方法中。調(diào)用domain對(duì)象的run方法時(shí)會(huì)先進(jìn)入enter里做如下處理:

exports.active = process.domain = this;
stack.push(this);
_domain_flag[0] = stack.length;

將當(dāng)期的domain對(duì)象設(shè)置成active并且綁定到process上,stack是一個(gè)保存domain對(duì)象的堆棧竿秆,用于domain嵌套使用的情況启摄,其中_domain_flag是一個(gè)用于js與c++進(jìn)行通信的對(duì)象。緊接著再執(zhí)行我們的業(yè)務(wù)代碼比如res.test()操作幽钢,此時(shí)便拋出了一個(gè)方法不存在的異常歉备。由于進(jìn)入enter方法后我們把當(dāng)前domain對(duì)象綁定到了process上,所以異常就交給domain的_errorHandler方法來處理了匪燕,回到之前的問題蕾羊,_errorHandler是如何處理異常的喧笔?
首先嘗試著讓之前綁定到domain上的error事件回調(diào)函數(shù)來處理該異常并清空當(dāng)前process的domain屬性,之所以所嘗試是因?yàn)榛卣{(diào)函數(shù)里可能又會(huì)拋出新的異常龟再,當(dāng)然了理想情況就是回調(diào)函數(shù)能夠很好的處理掉異常并且不拋出新的異常书闸,此時(shí)整個(gè)異常處理流程完美結(jié)束。如果有新的異常拋出利凑,先將對(duì)stack堆棧進(jìn)行出棧操作剔除已經(jīng)使用過的當(dāng)期domain對(duì)象浆劲,然后再看看棧里邊是否還存在domain對(duì)象,有的話就用棧訂上的domain又回到process._fatalException里繼續(xù)處理剛剛回調(diào)函數(shù)拋出的新異常哀澈。stack為空的話此時(shí)已經(jīng)沒有domain對(duì)象可以來處理異常牌借,至次本次異常處理以失敗結(jié)束然后繼續(xù)交給最開始講到的uncaughtException事件來處理。當(dāng)然了調(diào)用domain.run時(shí)并沒有拋出異常割按,那么domain也需要進(jìn)行出棧操作膨报,來抵消enter方法時(shí)的入棧操作以保持stack堆棧的平衡。

其實(shí)上面的reqd.add(res)和reqd.add(req)是可以不要的适荣,為什么可以不要呢现柠?在什么情況下需要什么情況下又不需要?ok弛矛,我們?cè)偕钊胙芯恳幌耫omain.add是如何工作的晒旅。官網(wǎng)中文檔有介紹domain.add接收emitter類型的參數(shù),也就是EventEmitter | Timer emitter or timer汪诉。為什么要這樣呢废恋,看下面的一段代碼

var EventEmitter = require('events').EventEmitter;
var e = new EventEmitter();
var timer = setTimeout(function () {
  e.emit('data');  
}, 1000);

function next() {
  e.once('data', function () {
    throw new Error('something wrong here');
  });
}

var d = domain.create();
d.on('error', function () {
  console.log('cache by domain');
});

d.run(next);

此時(shí)next函數(shù)里邊綁定到e對(duì)象上的data事件被觸發(fā)時(shí)domain對(duì)象是無法處理的,原因很明顯扒寄,data回調(diào)函數(shù)的運(yùn)行已經(jīng)處理domain.run方法之外鱼鼓。那我就要這個(gè)domain來處理錯(cuò)誤怎么辦呢,此時(shí)domain.add方法就派上用場(chǎng)了该编,我們只需要簡(jiǎn)單的調(diào)用一下d.add(e)或者d.add(timer)就可以解決這個(gè)問題迄本。domain.add方法為什么可以解決又是如何解決的呢?繼續(xù)往下看课竣。

當(dāng)調(diào)用domain.add(e)時(shí)嘉赎,如果上綁定有domain先移除再綁定新的domain,并將e對(duì)象加入新domain的members中于樟,從而保持著對(duì)e對(duì)象的引用公条。不管是timer對(duì)象還是event對(duì)象在觸發(fā)回調(diào)函數(shù)時(shí)都會(huì)先判斷是否有綁定domain對(duì)象

if (this.domain && this !== process)
    this.domain.enter();
callback();
if (this.domain && this !== process)
    this.domain.exit();

這些操作和domain.run方法相似,先執(zhí)行enter將domain對(duì)象綁定到process上迂曲,然后再執(zhí)行回調(diào)當(dāng)有異常發(fā)生時(shí)process會(huì)將異常傳到domain上處理靶橱,最后再調(diào)用exit方法將該domain移出stack堆棧。所以上面的代碼中必須得調(diào)用下d.add(e)或者d.add(timer)才會(huì)讓domain對(duì)象捕獲到回調(diào)中的異常。

整個(gè)Node.js異常處理就講到這里了关霸,其實(shí)在process._fatalException方法中調(diào)用domain來處理異常之前還進(jìn)行了一個(gè)異常處理操作
var caught = _errorHandler(er);
這個(gè)處理主要涉及到Node.js的異步隊(duì)列AsyncQueue在這里暫不做討論传黄,以后再做進(jìn)一步的研究,文章有點(diǎn)長(zhǎng)感謝能堅(jiān)持看到結(jié)尾的同學(xué)們队寇,不要吝嗇你們的贊哦~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膘掰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子佳遣,更是在濱河造成了極大的恐慌识埋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苍日,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窗声,警方通過查閱死者的電腦和手機(jī)相恃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笨觅,“玉大人拦耐,你說我怎么就攤上這事〖#” “怎么了杀糯?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)苍苞。 經(jīng)常有香客問我固翰,道長(zhǎng),這世上最難降的妖魔是什么羹呵? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任骂际,我火速辦了婚禮,結(jié)果婚禮上冈欢,老公的妹妹穿的比我還像新娘歉铝。我一直安慰自己,他們只是感情好凑耻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布太示。 她就那樣靜靜地躺著,像睡著了一般香浩。 火紅的嫁衣襯著肌膚如雪类缤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天邻吭,我揣著相機(jī)與錄音呀非,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岸裙,可吹牛的內(nèi)容都是我干的猖败。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼降允,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼恩闻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起剧董,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤幢尚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后翅楼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尉剩,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年毅臊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了理茎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡管嬉,死狀恐怖皂林,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚯撩,我是刑警寧澤础倍,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站胎挎,受9級(jí)特大地震影響沟启,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜犹菇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一美浦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧项栏,春花似錦浦辨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至列另,卻和暖如春芽腾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背页衙。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工摊滔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阴绢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓艰躺,卻偏偏與公主長(zhǎng)得像呻袭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腺兴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容