您现在的位置是:亿华云 > 人工智能

聊聊Nodejs的错误处理

亿华云2025-10-09 01:31:32【人工智能】8人已围观

简介本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。本文以连接错误ECONNREFUSED为例,看看nodejs对错误处理的过程。假设我们有以下代码constnet

本文转载自微信公众号「编程杂技」,聊聊理作者theanarkh。误处转载本文请联系编程杂技公众号。聊聊理  

本文以连接错误ECONNREFUSED为例,误处看看nodejs对错误处理的聊聊理过程。

假设我们有以下代码

const net = require(net); net.connect({ port: 9999}) 

如果本机上没有监听9999端口,误处那么我们会得到以下输出。聊聊理

events.js:170          throw er; // Unhandled error event          ^    Error: connect ECONNREFUSED 127.0.0.1:9999        at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1088:14)    Emitted error event at:        at emitErrorNT (internal/streams/destroy.js:91:8)        at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)        at processTicksAndRejections (internal/process/task_queues.js:81:17)  

我们简单看一下connect的误处调用流程。

const req = new TCPConnectWrap();   req.oncomplete = afterConnect;   req.address = address;   req.port = port;   req.localAddress = localAddress;   req.localPort = localPort;   // 开始三次握手建立连接   err = self._handle.connect(req,聊聊理 address, port);  

接着我们看一下C++层connect的逻辑

err = req_wrap->Dispatch(uv_tcp_connect,                   &wrap->handle_,                   reinterpret_cast(&addr),                   AfterConnect);  

C++层直接调用Libuv的uv_tcp_connect,并且设置回调是误处AfterConnect。接着我们看libuv的聊聊理实现。

do {        errno = 0;       // 非阻塞调用       r = connect(uv__stream_fd(handle),误处 addr, addrlen);     } while (r == -1 && errno == EINTR);     // 连接错误,判断错误码     if (r == -1 && errno != 0) {        // 还在连接中,聊聊理不是误处错误,等待连接完成,聊聊理事件变成可读       if (errno == EINPROGRESS)         ; /* not an error */       else if (errno == ECONNREFUSED)         // 连接被拒绝         handle->delayed_error = UV__ERR(ECONNREFUSED);       else         return UV__ERR(errno);     }     uv__req_init(handle->loop, req, UV_CONNECT);     req->cb = cb;     req->handle = (uv_stream_t*) handle;     QUEUE_INIT(&req->queue);     // 挂载到handle,等待可写事件     handle->connect_req = req;   uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);

我们看到Libuv以异步的方式调用操作系统,然后把request挂载到handle中,并且注册等待可写事件,云服务器提供商当连接失败的时候,就会执行uv__stream_io回调,我们看一下Libuv的处理(uv__stream_io)。

getsockopt(uv__stream_fd(stream),                  SOL_SOCKET,                  SO_ERROR,                  &error,                  &errorsize);   error = UV__ERR(error);   if (req->cb)       req->cb(req, error); 

获取错误信息后回调C++层的AfterConnect。

Localargv[5] = {       Integer::New(env->isolate(), status),      wrap->object(),      req_wrap->object(),      Boolean::New(env->isolate(), readable),      Boolean::New(env->isolate(), writable)    };    req_wrap->MakeCallback(env->oncomplete_string(), arraysize(argv), argv);   

接着调用JS层的oncomplete回调。

const ex = exceptionWithHostPort(status,                                    connect,                                    req.address,                                    req.port,                                    details);   if (details) {      ex.localAddress = req.localAddress;     ex.localPort = req.localPort;   }   // 销毁socket   self.destroy(ex); 

exceptionWithHostPort构造错误信息,然后销毁socket并且以ex为参数触发error事件。我们看看uvExceptionWithHostPort的实现。

function uvExceptionWithHostPort(err, syscall, address, port) {      const [ code, uvmsg ] = uvErrmapGet(err) || uvUnmappedError;     const message = `${ syscall} ${ code}: ${ uvmsg}`;     let details = ;     if (port && port > 0) {        details = ` ${ address}:${ port}`;     } else if (address) {        details = ` ${ address}`;     }     const tmpLimit = Error.stackTraceLimit;     Error.stackTraceLimit = 0;     const ex = new Error(`${ message}${ details}`);     Error.stackTraceLimit = tmpLimit;     ex.code = code;     ex.errno = err;     ex.syscall = syscall;     ex.address = address;     if (port) {        ex.port = port;     }     // 获取调用栈信息但不包括当前调用的函数uvExceptionWithHostPort,注入stack字段到ex中     Error.captureStackTrace(ex, excludedStackFn || uvExceptionWithHostPort);     return ex;   }   

我们看到错误信息主要通过uvErrmapGet获取

unction uvErrmapGet(name) {       uvBinding = lazyUv();      if (!uvBinding.errmap) {         uvBinding.errmap = uvBinding.getErrorMap();      }      return uvBinding.errmap.get(name);    }    function lazyUv() {       if (!uvBinding) {         uvBinding = internalBinding(uv);      }      return uvBinding;    } 

继续往下看,uvErrmapGet调用了C++层的uv模块的getErrorMap。

void GetErrMap(const FunctionCallbackInfo& args) {      Environment* env = Environment::GetCurrent(args);     Isolate* isolate = env->isolate();     Localcontext = env->context();     Local   // 从per_process::uv_errors_map中获取错误信息     size_t errors_len = arraysize(per_process::uv_errors_map);     // 赋值     for (size_t i = 0; i < errors_len; ++i) {         // map的键是 uv_errors_map每个元素中的value,值是name和message     const auto& error = per_process::uv_errors_map[i];       Localarr[] = { OneByteString(isolate, error.name),                             OneByteString(isolate, error.message)};      if (err_map               ->Set(context,                     Integer::New(isolate, error.value),                     Array::New(isolate, arr, arraysize(arr)))               .IsEmpty()) {          return;       }     }     args.GetReturnValue().Set(err_map);   } 

我们看到错误信息存在per_process::uv_errors_map中,我们看一下uv_errors_map的云南idc服务商定义。

struct UVError {    int value;   const char* name;   const char* message; }; static const struct UVError uv_errors_map[] = {    #define V(name, message) { UV_##name, #name, message},       UV_ERRNO_MAP(V)   #undef V   };   

UV_ERRNO_MAP宏展开后如下

{ UV_E2BIG, "E2BIG", "argument list too long"},   { UV_EACCES, "EACCES", "permission denied"},   { UV_EADDRINUSE, "EADDRINUSE", "address already in use"},   ……  

所以导出到JS层的结果如下

{      // 键是一个数字,由Libuv定义,其实是封装了操作系统的定义   UV_ECONNREFUSED: ["ECONNREFUSED", "connection refused"],       UV_ECONNRESET: ["ECONNRESET", "connection reset by peer"]      ...    }  

Node.js最后会组装这些信息返回给调用方。这就是我们输出的错误信息。那么为什么会是ECONNREFUSED呢?我们看一下操作系统对于该错误码的逻辑。

static void tcp_reset(struct sock *sk)   {        switch (sk->sk_state) {            case TCP_SYN_SENT:               sk->sk_err = ECONNREFUSED;               break;            // ...     }   }  

当操作系统收到一个发给该socket的rst包的时候会执行tcp_reset,我们看到当socket处于发送syn包等待ack的时候,如果收到一个fin包,则会设置错误码为ECONNREFUSED。我们输出的正是这个错误码。

很赞哦!(5)