Nodejs try catch捕捉异常失效场景的解决方案

1,179 阅读5分钟

nodejs是异步IO执行,所以我们将try/catch放置异步回调函数中,当出现一个异常时,try/catch操作只能捕获当次事件循环内的异常,我们通过try 拿到这个错误时错过了当前程序运行堆栈。(或者理解成,异步错误发生时在try catch块结束时候,所以当然不会被catch)

之后 Node 会触发 uncaughtException事件,而在node.js原生的uncaughtException 处理事件是挂在 process 对象上,所以,如果一个异常出现时,当前运行的 process 会直接挂掉,导致错误永远不会走到 catch语句.

比如,在实际项目中,

var deserialize = require('deserialize'); 
// 假设 deserialize 是一个带有 bug 的第三方模块
 
// app 是一个 express 服务对象
app.get('/users', function (req, res) {
    mysql.query('SELECT * FROM user WHERE id=1', function (err, user) {
        var config = deserialize(user.config); 
        // 假如这里触发了 deserialize 的 bug
        res.send(config);
    });
});

如果不幸触发了 deserialize 模块的 bug,这里就会抛出一个异常,最终结果是整个服务 crash。

当这种情况发生在 Web 服务上时结果是灾难性的。uncaughtException 错误会导致当前的所有的用户连接都被中断,甚至不能返回一个正常的 HTTP 错误码,用户只能等到浏览器超时才能看到一个no data received错误。

异常处理是程序运行中必须要关注的地方,当异常出现后,应该第一时间关注到,并且快速解决。大部分程序员们都不敢保证自己的代码百分比正确,所以应该在写代码时就要对异常提前做预防处理,尽量保证在异常出现时,给用户一个友好的提示,不至于服务挂起导致请求超时,并且能将异常信息做记录上报,方便后期排查解决。

一. 同步代码的异常捕获处理

// 同步代码中的异常使用try{}catch结构即可捕获处理。
try {
  throw new Error('错误信息');
} catch (e) {
  console.error(e.message);
}

// 可以正常捕获异常

二. 异步代码的错误处理

// try/catch 接口
// 异步代码下使用try{}catch结构捕获处理效果如何呢?
try {
  setTimeout(()=>{
    throw new Error('错误信息');
  })
} catch (e) {
  console.error('error is:', e.message);
}
//然而却没有捕获到异步错误。

那异步错误该怎么处理呢?首先换个思维,因为异常并不是事先准备好的,不能控制其到底在哪儿发生,所以站更高的角度,如监听应用进程的错误异常,从而捕获不能预料的错误异常,保证应用不至于奔溃调。

process.on('uncaughtException', (e)=>{
  console.error('process error is:', e.message);
});

如上代码从process上监听uncaughtException事件,可以捕获到整个进程包含异步中的错误信息,从而保证应用没有奔溃。

但是新的问题随之而来,因为异常不可预料的发生后,当异常出现时,直接从对应执行栈中断,而到process捕获的异常事件下,导致了v8引擎的垃圾回收功能不能按照正常流程工作,然后开始出现内存泄漏问题。

相对于异常来说,内存泄漏也是一个不能忽视的严重问题,而process.on('uncaughtException')的做法,很难去保证不造成内存的泄漏。所以当捕获到异常时,显式的手动杀掉进程,并开始重启node进程,即保证释放内存,又保证了保证服务后续正常可用。

process.on('uncaughtException', (e)=>{
  console.error('process error is:', e.message);
  process.exit(1);
  restartServer(); // 重启服务
});

但是上面的做法有一点直接,大家不免存疑惑,如果单进程单实例的部署下,杀掉进程在重启这一段时间内服务不能正常可用怎么办?这显然是不合理的。

其中的过程还参考了 async/awit 方案, 但是这个方案只擅长解决又固定返回的异常, 如 发现代码错误, 或者第三方库中有使用

Thow error, 还是会导致服务的异常中断,

三. 使用domain模块

domain模块,把处理多个不同的IO的操作作为一个组。注册事件和回调到domain,当发生一个错误事件或抛出一个错误时,domain对象会被通知,不会丢失上下文环境,也不导致程序错误立即退出,与process.on('uncaughtException')不同。

Domain 模块可分为隐式绑定和显式绑定:
隐式绑定: 把在domain上下文中定义的变量,自动绑定到domain对象
显式绑定: 把不是在domain上下文中定义的变量,以代码的方式绑定到domain对象

const domain = require('domain');
const d = domain.create();

d.on('error', (err) => {
  console.log('err', err.message);
  console.log(needSend.message);
});

const needSend = { message: '需要传递给错误处理的一些信息' };
d.add(needSend);

function excute() {
  try {
    setTimeout(()=>{
      throw new Error('错误信息');
    });
  } catch (e) {
    console.error('error is:', e.message);
  }
};

d.run(excute);

domin明显的优点,能把出问题时的一些信息传递给错误处理函数,可以做一些打点上报等处理工作,最起码保证重启后的服务,程序猿们知道发生了什么,有线索可查,也可以选择传递上下文进去,做一些后续处理。比如当服务出错的时候,可以把用户请求栈信息传给下游,返回告知用户服务异常,而不是用户一直等到请求自动超时。

参考文章

cnodejs.org/topic/55714…
segmentfault.com/a/119000004…