你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
xtx1130

node中关于Event Emitter的error事件和new Error之间,你所不知道的事 2个回复 专栏 @ Nodejs

xtx1130 发布于 6 月前

前言:很多人都讨论过promise的reject回调的错误没有详细错误栈的问题。其实我们在讨论这个问题的时候,忘记了promise出现的初衷--为了解决callback hell的问题,在浏览器端,promise主要为了解决ajax的问题,而在服务端,promise主要解决的便是Event Emitter的回调问题。而promise打印出的错误日志,便是Event Emitter中传递出来的error事件。

从两类不同的Error实例说起

我要说的第一类error便是 Event Emitter 中抛出的错误,在这里做一个简单的实验(本文中所有实例均基于node v8.0构建):

//内置的event Emitter 的 error
fs.open('xxx','r+',(err,data)=>{
if(err){
console.log(err)
}
})

这个例子返回的err为:

而我稍微变通一下,如果写入如下代码:

fs.open('xxx','r+',(err,data)=>{
if(err)
console.log(new Error(err))
});

这个例子返回的err为:

可以看到,在这个里面把详细的发生error的位置打印出来了,方便开发人员进行定位。
如果大家感觉这个例子不够直观的话,那么接下来看直接调用Event Emitter的例子:

const events = require('events').EventEmitter;
class Emit extends events{

}
var emits = new Emit();
emits.on('error',e=>console.log(e));
emits.emit('error',new Error('test'))
emits.emit('error',{Error:'error'})

我想大家也已经猜到了这个返回的大致内容的区别了吧:

第一个返回的错误栈非常详细,而第二个返回的仅仅是我emit的那个对象。以上几个例子的区别在于,一个调用了new Error来抛出错误,而另一种则是直接抛出Event Emitter的回调。

深入剖析Event Emitter中的错误处理方式

大家应该都知道,Event Emitter是基于libuv的,而http、fs、stream等很多模块都是基于Event Emitter的,正如第一节的例子,我们就以fs.open为切入点,来看一下底层libuv对错误的处理。
libuv中包括文件和网络两个常用的模块(其实还有高级轮询等,这里不做详细介绍),因为上面的例子用的是fs.open,那么我们fs.open为主线,来贯穿整套逻辑。首先便是src/unix/fs.c,这里面有libuv中所有fs相关方法的定义。

int uv_fs_open(uv_loop_t* loop,
               uv_fs_t* req,
               const char* path,
               int flags,
               int mode,
               uv_fs_cb cb) {
  INIT(OPEN);
  PATH;
  req->flags = flags;
  req->mode = mode;
  POST;
}

在这里简单介绍uv_fs_open各个参数或者类型的含义:uv_loop_t 便是一个event_loop了,一般来说用的都是uv_default_loop,uv_fs_t这个参数很重要,它里面包含了reslut,flag以及mode等域,接下来就是path了,这个意思大家应该都很明了,就是读取文件的路径,flags和mode就不多说了,就是标准的Unix Flags,uv_fs_cb就是callback了。
下面我们将视线转移到node的架构中src/node_file.cc

static void Open(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  int len = args.Length();
  if (len < 1)
    return TYPE_ERROR("path required");
  if (len < 2)
    return TYPE_ERROR("flags required");
  if (len < 3)
    return TYPE_ERROR("mode required");
  if (!args[1]->IsInt32())
    return TYPE_ERROR("flags must be an int");
  if (!args[2]->IsInt32())
    return TYPE_ERROR("mode must be an int");

  BufferValue path(env->isolate(), args[0]);
  ASSERT_PATH(path)

  int flags = args[1]->Int32Value();
  int mode = static_cast<int>(args[2]->Int32Value());

  if (args[3]->IsObject()) {
    ASYNC_CALL(open, args[3], UTF8, *path, flags, mode)
  } else {
    SYNC_CALL(open, *path, *path, flags, mode)
    args.GetReturnValue().Set(SYNC_RESULT);
  }
}

我们可以发现,真正的调用在ASYNC_CALL,这里可能读者好奇了,我们只有三个参数啊,为何会走到args[3]的判断中,一会下面到js的源码中再跟大家剖析一下。一步一步进行溯源,发现node端真正对uv_fs_open进行调用的地方在上面的宏定义中:

#define ASYNC_DEST_CALL(func, request, dest, encoding, ...)                   \
  Environment* env = Environment::GetCurrent(args);                           \
  CHECK(request->IsObject());                                                 \
  FSReqWrap* req_wrap = FSReqWrap::New(env, request.As<Object>(),             \
                                       #func, dest, encoding);                \
  int err = uv_fs_ ## func(env->event_loop(),                                 \
                           req_wrap->req(),                                   \
                           __VA_ARGS__,                                       \
                           After);                                            \
  req_wrap->Dispatched();                                                     \
  if (err < 0) {                                                              \
    uv_fs_t* uv_req = req_wrap->req();                                        \
    uv_req->result = err;                                                     \
    uv_req->path = nullptr;                                                   \
    After(uv_req);                                                            \
    req_wrap = nullptr;                                                       \
  } else {                                                                    \
    args.GetReturnValue().Set(req_wrap->persistent());                        \
  }

在这里读者需要注意下uv_fs_##func实现了对uv_fs_open 真正的调用,因为开始的代码为了触发错误,所以req->result 必小于0,即为进入到err<0的逻辑中,在这里,又对After进行了调用,注意After中有一段代码:

if (req->result < 0) {
    // An error happened.
    argv[0] = UVException(env->isolate(),
                          req->result,
                          req_wrap->syscall(),
                          nullptr,
                          req->path,
                          req_wrap->data());
  } else {
 //...
 }

在这里,对错误捕捉用的是UVException,而没有用throwUVException,主要作用在于这个错误是系统错误,并非js的问题,监听error事件完全可以监听到,没有必要throw出来,而在UVException的定义中,会把整体错误进行整合并变成object抛给error事件的回调:

Local<Object> e = Exception::Error(js_msg)->ToObject(isolate);

  e->Set(env->errno_string(), Integer::New(isolate, errorno));
  e->Set(env->code_string(), js_code);
  e->Set(env->syscall_string(), js_syscall);
  if (!js_path.IsEmpty())
    e->Set(env->path_string(), js_path);
  if (!js_dest.IsEmpty())
    e->Set(env->dest_string(), js_dest);

node中对fs此类错误的处理

扒完了c++部分的流程,接下来node部分的就比较简单了,视线移到/lib/fs.js,这是require('fs')的fs文件,其中比较瞩目的便是:

const constants = process.binding('constants').fs;
const fs = exports;
Object.defineProperty(exports, 'constants', {
  configurable: false,
  enumerable: true,
  value: constants
});

可以发现,fs中,js调用c++的方法通过的是process.binding,所以说process对象中封装的都是底层c++的方法,其中fs.open的源码也是相当简单:

fs.open = function(path, flags, mode, callback_) {
  var callback = makeCallback(arguments[arguments.length - 1]);
  mode = modeNum(mode, 0o666);

  if (handleError((path = getPathFromURL(path)), callback))
    return;
  if (!nullCheck(path, callback)) return;

  var req = new FSReqWrap();
  req.oncomplete = callback;

  binding.open(pathModule._makeLong(path),
               stringToFlags(flags),
               mode,
               req);
};

在这里,mode参数被处理了,如果没有参数的话则会返回0o666,所以说,我们刚才会走到ASYNC_CALL这个逻辑中。重点主要放到makeCallback这个函数身上:

function makeCallback(cb) {
  if (cb === undefined) {
    return rethrow();
  }

  if (typeof cb !== 'function') {
    throw new TypeError('"callback" argument must be a function');
  }

  return function() {
    return cb.apply(null, arguments);
  };
}

这里返回的是一个函数,并没有对cb进行调用,而是apply了一下arguments,接下来我们移步到刚才的函数,发现在handleError中进行了callback的调用:

function handleError(val, callback) {
  if (val instanceof Error) {
    if (typeof callback === 'function') {
      process.nextTick(callback, val);
      return true;
    } else throw val;
  }
  return false;
}

这里有一个非常重要的地方 val instanseof Error ,说明libuv生成的error也是Error的实例。还有这里调用的nexttick来执行的回调而且也没有将错误抛出。通过这一圈源码观光,我们可以发现一个很有趣的地方,开始我们测试的两个都为Error的实例,但是为什么内容不一样呢?这里引申出来了一个概念,就是error分为两种:javascript错误和系统错误,而fs.open 属于第二种---系统错误。系统错误error实例的stack方法返回的是libuv中定义好的错误的返回,而不是javascript错误中关于stack的返回,所以说凡是涉及到Event Emitter的错误,我们都需要做一层new Error,强制转换成javascript错误进行返回,这样可以以最快的速度定位到问题的原因出在哪里。
by:小菜

登录后回复,如无账号,请使用邀请码注册