Node.js之异常处理

   记得刚刚开始学Node.js时自己尝试着写了一个简单的http服务器,跟以前接触过的php相比感觉更自由,编起码来也更爽了。但是某天发现稍微一个很小的错误就导致整个http进程挂掉了,顿时有种不靠谱的感觉啊,跟php比起来感觉Node.js容错能力确实弱了很多,起码一个php文件出错也不会导致所有的服务都挂掉。

   

      后来接触到Node.js web开发框架后感觉也不是那么轻易就让整个进程都挂掉的,于是便想研究下Node.js究竟是如何来处理各种异常从而避免整个进程挂掉的。

     

     当我们的程序运行在Node.js进程里不小心抛出一个异常时便会触发process对象的_fatalException方法,并将异常对象err传进去,_fatalException方法主要做以下一些处理:

 

当process对象上有绑定domain时便调用domain对象的_errorHandler方法来处理,

 

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

 

_errorHandler会返回一个布尔值来通知当前程序domain是否有对该异常进行处理,如果domain没有做处理,此时process对象便会触发一个绑定到process上的uncaughtException事件来处理该异常,并且同样会返回一个布尔值来通知当前程序是否有对异常进行处理。

 

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

 

走到这个地步时如果异常还没被正常的处理那么此时process就有点不高兴了,既然你们都不处理那我就准备让你们全部挂掉吧!(确实太狠了点啊),这个时候悲剧即将发生。。。

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

 

如果异常都被妥妥的处理掉了那么Node.js进程便会处理当前事件的收尾的工作,比如调用process.nextTick传进去的回调函数在这个时候就准备被调用了,然后继续执行事件队列里的下一个事件

t = setImmediate(process._tickCallback)

总结下来Node.js中异常处理流程大概就是这样的:

 

这整个过程中有个很重要的处理环节没有加上去,那就是上面提到的domain对象。
首先简单介绍下domain对象的使用场景以及基本使用方法:

 

当我们开启一个Node.js的http服务器时不可避免的会出现各种我们没有预期到的异常,并且我们预先写好的try catch也无法捕捉。这时最关键的是如何保证整个服务进程不会挂掉,并且能够很友好的反馈给浏览器端的用户。尽管process对象提供了一个uncaughtException事件方法让我们可以处理异常并且保证当前的服务进程不会挂掉,但由于丢失了当前的上下文,说得直接点就是丢失了response对象很难向用户及时并且友好的输出错误提示,此时便陷入了用户会一直傻傻的等待服务器超时(早就关闭网站了)的尴尬场景。

 

有了domain模块我们便可以很方便的处理上面描述的场景了,刚刚开始接触domain这个模块时真不知道是个啥东西,名字都叫的怪怪的。后来去翻了先官网上有关domain的文档才知道这货到底有啥作用,我们就依照官网的示例来说明domain如何处理上述场景:

 

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);
      }
 });

当res对象调用各种方法产生异常时,之前创建好的domain对象reqd便会收到通知,从而触发我们预先设置好的处理方法来即使并且友好的输出给用户,避免超时这种糟糕的用户体验!对于domain对象其他的方法大家可以直接翻看Node.js官网文档的介绍,我这里就不啰嗦了~

 

下面我们着重的来研究下domain对象为何如此神奇?

当我们require('domain')对象时便对event模块的EventEmitter对象产生了影响

EventEmitter.usingDomains = true;

紧跟着对process的domain属性进行了覆盖

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

domain模块本身维护着一个存放domain对象的数组 _domain,再接着就是告诉process对象要使用到domain了

process._setupDomainUse(_domain, _domain_flag);

调用这个方法后影响到的地方可不少,之前我们说过Node.js每个事件都会调用一下_tickCallback来处理之前调用process.nextTick保存到事件队列里的回调函数,现在Node.js不调用了这个了,换成了调用_tickDomainCallback方法来代替_tickCallback。继续我们的domain模块,当创建一个新的domain对象时便初始化了的它的members属性来存放该domain要守护的对象,对照着上述的代码

 

var reqd = domain.create();

此时reqd.members=[], 于是我们调用add方法将req已经res对象都添加到domain中,由domain来帮他们处理各种错误。

reqd.add(req);
reqd.add(res);

接着告诉domain当req或者res操作出异常时应该如何处理

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);
      }
});

其实就上面那样还是没法捕获到异常,甚至都无法响应,因为我们还没调用res.write或者res.end方法来向用户输出内容,就算我们加上

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

依然无法像我们预期想象的那样进入异常处理回调方法里,别忘了将可能发生异常的代码放入domain.run中来执行,就像这样的:

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);

此时一切都已就绪,万事俱备只欠东风了,就等着各种异常来临了。ok, 此时由于res的某个操作(比如调用不存在的test方法)导致了一个异常的产生。根据最开始描述的处理流程,这个异常会被Node.js进程传到process._fatalException中进行处理,如果process上绑定有domain对象则会调用domain的_errorHandler方法来处理异常,那_errorHandler究竟如火如荼处理异常的呢?在讨论这个问题之前我们先回到上面的reqd.run方法中。调用domain对象的run方法时会先进入enter里做如下处理:

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

将当期的domain对象设置成active并且绑定到process上,stack是一个保存domain对象的堆栈,用于domain嵌套使用的情况,其中_domain_flag是一个用于js与c++进行通信的对象。紧接着再执行我们的业务代码比如res.test()操作,此时便抛出了一个方法不存在的异常。由于进入enter方法后我们把当前domain对象绑定到了process上,所以异常就交给domain的_errorHandler方法来处理了,回到之前的问题,_errorHandler是如何处理异常的?

首先尝试着让之前绑定到domain上的error事件回调函数来处理该异常并清空当前process的domain属性,之所以所尝试是因为回调函数里可能又会抛出新的异常,当然了理想情况就是回调函数能够很好的处理掉异常并且不抛出新的异常,此时整个异常处理流程完美结束。如果有新的异常抛出,先将对stack堆栈进行出栈操作剔除已经使用过的当期domain对象,然后再看看栈里边是否还存在domain对象,有的话就用栈订上的domain又回到process._fatalException里继续处理刚刚回调函数抛出的新异常。stack为空的话此时已经没有domain对象可以来处理异常,至次本次异常处理以失败结束然后继续交给最开始讲到的uncaughtException事件来处理。当然了调用domain.run时并没有抛出异常,那么domain也需要进行出栈操作,来抵消enter方法时的入栈操作以保持stack堆栈的平衡。

 

其实上面的reqd.add(res)和reqd.add(req)是可以不要的,为什么可以不要呢?在什么情况下需要什么情况下又不需要?ok,我们再深入研究一下domain.add是如何工作的。官网中文档有介绍domain.add接收emitter类型的参数,也就是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);

此时next函数里边绑定到e对象上的data事件被触发时domain对象是无法处理的,原因很明显,data回调函数的运行已经处理domain.run方法之外。那我就要这个domain来处理错误怎么办呢,此时domain.add方法就派上用场了,我们只需要简单的调用一下d.add(e)或者d.add(timer)就可以解决这个问题。domain.add方法为什么可以解决又是如何解决的呢?继续往下看。

 

当调用domain.add(e)时,如果上绑定有domain先移除再绑定新的domain,并将e对象加入新domain的members中,从而保持着对e对象的引用。不管是timer对象还是event对象在触发回调函数时都会先判断是否有绑定domain对象

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

这些操作和domain.run方法相似,先执行enter将domain对象绑定到process上,然后再执行回调当有异常发生时process会将异常传到domain上处理,最后再调用exit方法将该domain移出stack堆栈。所以上面的代码中必须得调用下d.add(e)或者d.add(timer)才会让domain对象捕获到回调中的异常。

 

整个Node.js异常处理就讲到这里了,其实在process._fatalException方法中调用domain来处理异常之前还进行了一个异常处理操作

var caught = _errorHandler(er);

这个处理主要涉及到Node.js的异步队列AsyncQueue在这里暂不做讨论,以后再做进一步的研究,文章有点长感谢能坚持看到结尾的同学们,不要吝啬你们的赞哦~

该文章来自于阿里巴巴技术协会(ATA)

作者:淘杰

时间: 2024-09-24 12:09:06

Node.js之异常处理的相关文章

解析Node.js异常处理中domain模块的使用方法_node.js

NodeJS 提供了 domain 模块,可以简化异步代码的异常处理.在介绍该模块之前,我们需要首先理解"域"的概念.简单的讲,一个域就是一个 JS 运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出.NodeJS 通过 process 对象提供了捕获全局异常的方法,示例代码如下 process.on('uncaughtException', function (err) { console.log('Error: %s', err.message); });

使用Node.js完成的第一个项目的实践总结

项目简介 这是一个资产管理项目,主要的目的就是实现对资产的无纸化管理.通过为每个资产生成二维码,来联合移动终端完成对资产的审核等.这个项目既提供了Web端的管理界面也提供移动端(Andorid)的资产审核.派发等相关功能. 我们用Node.js构建该项目的Web端以及移动端的Serveice API. 项目主框架:Express 简介 Express 是一个非常流行的node.js的web框架.基于connect(node中间件框架).提供了很多便于处理http请求等web开发相关的扩展. Ex

DockOne微信分享(六十九):微服务选型之Modern Node.js

本文讲的是DockOne微信分享(六十九):微服务选型之Modern Node.js[编者的话]目前Node.js的发展非常快,大家可能还停留在:Node.js性能很好,Node.js里都是回调,写起来很恶心,Node.js只能做前端工具,Node.js是单线程部署会有问题,以及这样的八卦<uber用go替代Node.js重写了地理位置服务>... 可是真相呢? 在微服务盛行的今天,为什么我们要选用Node.js去构建微服务呢?本次分享将试图从以下2个方面给出答案: 被误解的Node.js:除

端午节后福利:Node.js 8

端午节结束了.虽然接下来的四个月都没有节假日,但笔者一点都不烦恼.因为 Node.js 8 在端午后第一个工作日就正式发布,这足以让我与 Node.js 的激情燃烧一个夏天!本文挑选了笔者认为 Node.js 8 最令人兴奋的四大新功能,与大家分享. async/await 与 util.promisify Node.js 一直以来的关键设计就是把用户关在一个"异步编程的监狱"里,以换取非阻塞 I/O 的高性能,让用户轻易开发出高度可扩展的网络服务器.这从 Node.js 的 API

深入了解Node.js中的一些特性_node.js

Node.js作为一门新兴的后台语言,旨在帮助程序员快速构建可伸缩的应用程序.Node.js有很多吸引人的地方,有关它的报道不计其数,本文将针对EventEmitter.Streams.Coding Style.Linting.Coding Style等特性进行分析探讨,帮助用户对Node.js有更深入的了解. 作为一个基于Chrome JavaScript 运行时建立的平台,我们对JavaScript 的相关认识,似乎都可应用于node应用程序之上:无需额外的语言扩展或修饰,我们便可以把前端编

深入分析 Node.js 异步流程控制教程

摘要 目前在js流程控制领域越来越乱,各种派系...比如promise,generator,async函数,各种混战,在百花齐放的今天,作为前端或Node.js沾边工程师或全栈工程师,你知道该学哪种么? 从下一代测试框架ava说起 流程控制发展的前世今生概览 从co引出的血案,到yieldable 5种,到aysnc函数,聊聊同步的流程控制 最后推导一下学习重点.未来趋势 个人介绍 i5ting(桑世龙),空弦科技 CTO,StuQ 明星讲师,开源项目 Moajs 作者,Node.js 技术布道

将Node.js项目docker容器化并纳入kubernetes调度编排的实践

  简述 此文档以XXXLogApi-nj项目为例,讲解了将基于Node.js+Express开发的javascript项目容器化的过程.希望以后类似的项目可以以此为参照进行扩展. XXXLogApi-nj本身是一个微服务化的项目,其作用是为系统单纯的收集相关发布日志,以便能及时的展示给用户. ***这份文档的操作,开始于编码完成之后流程.不涉及GIT和JENKINS的等的操作. ***为保持职业操作,涉及公司信息的地方作了敏感化处理. ***在这个系列中,我同时作了spring boot, b

Node.js 服务端实践之 GraphQL 初探

0.问题来了 DT 时代,各种业务依赖强大的基础数据平台快速生长,如何高效地为各种业务提供数据支持,是所有人关心的问题. 现有的业务场景一般是这样的,业务方提出需求,然后寻找开发资源,由后端提供数据,让前端实现各种不同的业务视图.这样的做法存在很多的重复劳动,如果能够将其中通用的内容抽取出来提供给各个业务方反复使用,必然能够节省宝贵的开发时间和开发人力. 前端的解决方案是将视图组件化,各个业务线既可以是组件的使用者,也可以是组件的生产者.那么问题来了,前端通过组件实现了跨业务的复用,后端接口如何

聊天-ASP.net MVC的一个项目里可以使用Node.js做的应用吗?

问题描述 ASP.net MVC的一个项目里可以使用Node.js做的应用吗? 20C 是这样的,最近我们小组在做一个ASP.net MVC 的项目网页,里面有个即时网络聊天室的功能要实现,我看到用Node.js做好像很不错,但是就是不知道.net的项目可以使用吗?