NodeJS中的错误处理

所需知识

  • 假设你已经熟悉JS、Java、Python、C++或者其他类似的语言中关于异常的知识,并了解throw和catch的意思
  • 假设你已经熟悉NodeJS,并且对于异步操作以及诸如callback(err,result)这种完成异步操作的处理模式
  • 假设你知道如下这种模式为什么无法处理异常
     function myApiFunc(callback) {
  /*
   * This pattern does NOT work!
   */
  try {
    doSomeAsynchronousOperation((err) => {
      if (err) {
        throw (err);
      }
      /* continue as normal */
    });
  } catch (ex) {
    callback(ex);
  }
}
  • 假设你已经熟悉NodeJS中4种传递、分发错误的模式
    • throw错误,我们一般称之为异常
    • 传递error对象到回调函数,回调函数会被用来处理异常和异步操作的结果
    • 向一个reject Promise函数传递error对象
    • EventEmitter中发出一"error"事件

我们接下来会讨论如何使用这些模式,本文并不要求你了解关于domain的任何事情。除此之外,你还需要认识到NodeJS中exception异常并不等于error错误,一个error对象是Error类的一个实例,error对象通常被传递给另外的处理函数或者被throw抛出,当你throw一个error时,它就会变成exception。

 throw new Error('something bad happened');

在NodeJS中,在异步操作中更常见的处理模式是

callback(new Error('something bad happened'));

不过伴随着async/await的流行和NodeJS进步,这种模式正在取代中

操作错误 VS 程序员错误

一般说来,区分程序员错误和操作错误是很有价值的。

  • 操作错误代表那类运行时才会报错的正确代码,这些不是程序的Bug,事实上,这一般是发生错误的通常原因,out of memory和打开太多文件、系统配置、网络原因或者依赖的外部服务存在500 error、连接失败的情况
    • 无法连接至服务器
    • 无法resolve 主机名称(找不到IP
    • 无效的用户输入
    • 请求超时
    • 服务端返回500
    • socket挂起
    • 系统内存爆满out of memory
  • 编程错误、程序员错误,这一般就是我们的bugs
    • 试图读取undefined
    • 调用异步函数却未触发回调
    • 参数不匹配

虽然人们经常会拿这两种error来统称,但是他们真的不一样。操作错误是那些程序在正常运行过程中必须解决的问题,只要解决问题就不会出现更加严重问题。相反的,程序员错误就是BUG,这些情况可能导致犯错,理解这些有助于正确处理错误

处理操作错误

首先必须认清的是与解决性能问题一样,你很难集中式的处理错误。处理操作错误的关键是在于能够准确认识到错误发生的影响和原因。
一般情况下,可能在调用栈的不同层次来处理相同的错误,但不意味着就要将全部错误上抛给顶层来处理,因为有的时候在顶层并不清楚之前的操作哪些部分已经完成哪些部分没完成

一般情况下,可以按照以下模式来处理

  • 直接处理错误。如果你知道如何处理的话
  • 将错误传播。如果你不知道如何处理错误,最简单的办法就是中止当前操作、清理下现场,然后把问题抛给上层来处理。
  • 重试操作。对于某些因为网络或者远程服务不可用的问题,通常来说需要做的事情是就是重试那个出错的操作。如果外部服务返回503(Service Unavailable),那么需要考虑重试几下。如果你要打算重试,那么你应该正在文档中明确指出你需要重试的次数以及在重试过程中等待的次数。另外不要让自己一直在重试某个操作
    此外千万不要在整个调用栈中的每一次都去做重试,最好的方式如果深层次就不要去重试,交给浅层次来重试,以避免无休止的等待
  • 爆炸 对于某些确实不应该发生的错误,比如无法连接到localhost,最好的方式就是记录日志然后crash就可以了。其他错误比如对JS来说很难处理的问题--内存溢出那么就应该主动选择崩溃。
  • 记录错误日志,然后什么都不做 对于有些事情你能做的事情很少,比如既不需要你重试也不需要终止请求,也没必要crash掉进程,那么你做的事情就是只需要记录日志,必须要记录跟这个事情相关的一些信息。

处理程序员错误

对于程序员错误来说,最直接的处理方式就是什么都不要做,让程序崩掉
如果你要解决这类错误就要花更多的时间写更多的异常处理代码来处理,只需要配置好能够自动重启的守护脚本即可完成工作
一般这类错误就是Bug,所以最有价值的事情就是调试修复它而不是掩盖。
另外在可靠的分布式系统中,客户端必须能够通过重试、重连来规避服务端的这类问题

编写异常处理代码

  • throw抛出的错误一般是同步的,可能由调用者try/catch来处理,如果调用者没有做这个操作,那么可能被process的uncaughtException或者domain来捕捉处理
  • callback是处理异步错误比较常见的模式
  • 自NodeJS8.0以来,async/await是最常见处理异步错误的模式。
  • 在更加复杂的一些情况里面,使用返回EventEmitter对象的方案来处理
    • 比如你要做的复杂操作里面会产生多种错误或者多种结果,那么就需要了
    • 对于复杂的需要处理多种异步操作的状态机对象来说,就会需要。一个socket对象在NodeJS中就是一个event emitter对象,它能提供connect、end、timeout、drain和close事件,当然也提供了error事件。

Domain和process.on('uncaughtException')

不推荐使用,这是一种盲处理错误的模式。

推荐错误处理的模式

  1. 明确你的函数是做什么的?如果不满足的话,直接生成错误对象吧
    • 期望什么参数
    • 每个参数的类型
    • 任何其他期望的限制
  2. 使用Error或者其子类对象,并实现Error协议。
  3. 使用Error的name属性来区分error
  4. 通过给error对象的属性赋值来描述更加完整的错误
  5. 如果你想传递深层次的error给调用者,那么就应该考虑包装下它