Table of Contents

  1. 背景
  2. nodejs中异常抛出和处理的5种方式
  3. 异常的2大分类和处理策略
    1. 操作失败
    2. 程序员失误
    3. 处理失败
  4. 何时以什么方式抛出异常(Throw, Callback 还是 EventEmitter)
  5. 编写函数时的建议
  6. Reference

背景

Nodejs是单线程,这会导致如果exception没有合适的处理,会导致整个程序的退出。所以要谨慎处理exception。以及区分不同的异常和何时使用哪种异常抛出方式。


nodejs中异常抛出和处理的5种方式

  1. Throw the error as exception
    这是最熟悉的一种异常方式,跟其他的编程语言时类似的。这类异常如果没有被处理,会导致程序的退出。
    1
    2
    3
    4
    5
    6
    function calculateSquare(number, callback) {
    if (typeof number !== "number") {
    throw new Error("Argument of type number is expected."));
    };
    return number * number;
    }
  2. Pass the error to a callback
    callback是处理异步操作的返回结果的。把错误传给一个callback,这个函数正是为了处理异常和处理异步操作返回结果的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function calculateSquare(number, callback) {
    setTimeout(() => {
    if (typeof number !== "number") {
    callback(new Error("Argument of type number is expected."));
    return;
    }
    const result = number * number;
    callback(null, result);
    }, 1000);
    }
    let callback = (error, result) => {
    if (error !== null) {
    console.log("Caught error: " + String(error));
    return;
    }
    console.log(result);
    };
    calculateSquare('s', callback);
  3. Pass the error to a reject Promise function(ES6出现了Promise)
    Promise中reject返回的内容可以被catch拿到,resolve返回的内容可以被then拿到
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function calculateSquare(number) {
    return new Promise((resolve,reject)=>{
    if (typeof number !== "number"){
    reject('Argument of type number is expected')
    }else{
    const result = number * number;
    resolve(result)
    }
    })
    }
    calculateSquare(2).
    then(res=>{
    console.log(res);
    }).
    catch(error=>{
    console.log(error);
    })
  4. Emit an error event on an EventEmitter
    通过EventEmitter可以定义自己的事件,事件触发和事件监听。当EventEmitter(事件)内发生错误时,典型的操作是发出error事件,如果该事件没有监听error事件,那么当error事件被发射时,node进程将退出。为了防止node进程退出,应该为error事件添加监听器。
    1
    2
    3
    4
    5
    6
    7
    8
    const emitter = require('events')
    const myEmitter = new emitter();
    myEmitter.on('error',(err)=>{
    console.log('whoops! there was an error')
    })
    myEmitter.emit('error',new Error('whoops'));
    //通过发射error而抛出的异常必须通过监听error事件捕获或
    //process.on('uncaughtException', (err),try catch则不行。
  5. Async/Await(ES7)
    可以通过catch同步代码的方式catch异步代码了,不需要写then。Resolve的参数就是返回值,reject的参数就是catch里的error.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function calculateSquare(number) {
    return new Promise((resolve,reject)=>{
    if (typeof number !== "number"){
    reject('Argument of type number is expected')
    }else{
    const result = number * number;
    resolve(result)
    }
    })
    }
    (async function test(){
    try {
    let res = await calculateSquare('s');
    console.log(res);
    } catch (error) {
    console.log(error)
    }
    })()

异常的2大分类和处理策略

操作失败

不是程序的bug,是系统本身(oom…),网络问题,远程服务(5xx,ECONNREFUSED…)

  • 连接不到服务器
  • 无法解析主机名
  • 无效的用户输入
  • 请求超时
  • 服务器返回500
  • 套接字被挂起
  • 系统内存不足

    程序员失误

    是程序里的Bug。这些错误往往可以通过修改代码避免。它们永远都没法被有效的处理。
  • 读取 undefined 的一个属性
  • 调用异步函数没有指定回调
  • 该传对象的时候传了一个字符串
  • 该传IP地址的时候传了一个对象

    处理失败

  • 直接处理
    有的时候该做什么很清楚。如果你在尝试打开日志文件的时候得到了一个ENOENT错误,很有可能你是第一次打开这个文件,你要做的就是首先创建它。更有意思的例子是,你维护着到服务器(比如数据库)的持久连接,然后遇到了一个“socket hang-up”的异常。这通常意味着要么远端要么本地的网络失败了。很多时候这种错误是暂时的,所以大部分情况下你得重新连接来解决问题。(这和接下来的重试不大一样,因为在你得到这个错误的时候已经不必要重试了)
  • 把出错扩散到客户端
    如果你不知道怎么处理这个异常,最简单的方式就是放弃你正在执行的操作,清理所有开始的,然后把错误传递给客户端。(怎么传递异常是另外一回事了,接下来会讨论)。这种方式适合错误短时间内无法解决的情形。比如,用户提交了不正确的JSON,你再解析一次是没什么帮助的。
  • 重试操作
    对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题。比如,远程服务返回了503(服务不可用错误),你可能会在几秒种后重试。如果确定要重试,你应该清晰的用文档记录下将会多次重试,重试多少次直到失败,以及两次重试的间隔。 另外,不要每次都假设需要重试。如果在栈中很深的地方(比如,被一个客户端调用,而那个客户端被另外一个由用户操作的客户端控制),这种情形下快速失败让客户端去重试会更好。如果栈中的每一层都觉得需要重试,用户最终会等待更长的时间,因为每一层都没有意识到下层同时也在尝试。
  • 直接崩溃
    对于那些本不可能发生的错误,或者由程序员失误导致的错误(比如无法连接到同一程序里的本地套接字),可以记录一个错误日志然后直接崩溃。其它的比如内存不足这种错误,是JavaScript这样的脚本语言无法处理的,崩溃是十分合理的。(即便如此,在child_process.exec这样的分离的操作里,得到ENOMEM错误,或者那些你可以合理处理的错误时,你应该考虑这么做)。在你无计可施需要让管理员做修复的时候,你也可以直接崩溃。如果你用光了所有的文件描述符或者没有访问配置文件的权限,这种情况下你什么都做不了,只能等某个用户登录系统把东西修好。
  • 记录错误,其他什么都不做
    有的时候你什么都做不了,没有操作可以重试或者放弃,没有任何理由崩溃掉应用程序。举个例子吧,你用DNS跟踪了一组远程服务,结果有一个DNS失败了。除了记录一条日志并且继续使用剩下的服务以外,你什么都做不了。但是,你至少得记录点什么(凡事都有例外。如果这种情况每秒发生几千次,而你又没法处理,那每次发生都记录可能就不值得了,但是要周期性的记录)。
  • 立刻崩溃,重启
    崩溃是失误来临时最快的恢复可靠服务的方法。奔溃应用程序唯一的负面影响是相连的客户端临时被扰乱。在一个完备的分布式系统里,客户端必须能够通过重连和重试来处理服务端的错误。不管 NodeJS 应用程序是否被允许崩溃,网络和系统的失败已经是一个事实了。

何时以什么方式抛出异常(Throw, Callback 还是 EventEmitter)

  • throw以同步的方式传递异常–也就是在函数被调用处的相同的上下文。如果调用者(或者调用者的调用者)用了try/catch,则异常可以捕获。如果所有的调用者都没有用,那么程序通常情况下会崩溃(异常也可能会被进程级的uncaughtException)。
  • Callback 是最基础的异步传递事件的一种方式。用户传进来一个函数(callback),之后当某个异步操作完成后调用这个 callback。通常 callback 会以callback(err,result)的形式被调用,这种情况下, err和 result必然有一个是非空的,取决于操作是成功还是失败。
  • 更复杂的情形是,函数没有用 Callback 而是返回一个 EventEmitter 对象,调用者需要监听这个对象的 error事件。这种方式在两种情况下很有用。
  • 当你在做一个可能会产生多个错误或多个结果的复杂操作的时候。比如,有一个请求一边从数据库取数据一边把数据发送回客户端,而不是等待所有的结果一起到达。在这个例子里,没有用 callback,而是返回了一个 EventEmitter,每个结果会触发一个row 事件,当所有结果发送完毕后会触发end事件,出现错误时会触发一个error事件。
  • 用在那些具有复杂状态机的对象上,这些对象往往伴随着大量的异步事件。例如,一个套接字是一个EventEmitter,它可能会触发“connect“,”end“,”timeout“,”drain“,”close“事件。这样,很自然地可以把”error“作为另外一种可以被触发的事件。在这种情况下,清楚知道”error“还有其它事件何时被触发很重要,同时被触发的还有什么事件(例如”close“),触发的顺序,还有套接字是否在结束的时候处于关闭状态。
  • callback和EventEmitter通常2选1。

编写函数时的建议

清楚的知道函数是做什么的,预期参数—参数的类型—参数的额外约束—返回值—
还要知道:调用者可能会遇到的操作失败,怎么处理这个失败(抛出,传递给callback,emitter)。
使用ERROR对象或它的子类,并且实现 Error 的协议
所有错误要么使用 Error 类要么使用它的子类。你应该提供name和message属性。建议在捕获一些比较底层的异常时封装一层,将原始的error转换成自己的error类,方便调用者理解和处理。


Reference

https://www.joyent.com/node-js/production/design/errors
https://code.oneapm.com/nodejs/2015/04/13/nodejs-errorhandling/