Fork me on GitHub

After code
  • Geeker
  • Gamer
  • JS
  • C
  • Node
  • React
  • Hippop
  • TDD
That Is An Byte of Me

[翻译] node.js 的 UnhandledPromiseRejectionWarning

05 Apr 2017

TLDR;

Promise的错误处理不当, 会导致很多隐藏的Bug. 所以在 Node.js 6.6.0 之后, 如果原生的Promise 没有catch Rejection 就会在 console 打印一条警告; 可以监听 Node 进程 unhandledRejection 事件来获得 Rejection 的 Error 信息和关闭警告. Async/await 可以让Promise处理更加方便, 但是也一定 catch async 函数的返回的Promise; async 函数的返回的是一个原生的Promise, 无法被替换成其他第三方的 Promise 实现.

译文

自 Node.js 6.6.0 添加了一个特性(或者说是bug): 默认输出未处理的Rejection的Promise到标准输出; 这个特性会偶尔有用. 简单点说就是下面的代码会有一个标准输出打印一个错误日志.

// node -v  6.10.0
Promise.reject(new Error('woops'));

/*
$ node test.js
(node:83530) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: woops
*/

之前知名Promise库 bluebird 也有类似的功能. 在我之前的issue曾经表示非常讨厌这个功能, 但它只是我不是用 bluebird 的各种原因的其中一个. 可是这个功能现在已加入到 node 中了, 所以接下来的日子我们将无法摆脱这个 “bug”. 与其怨天尤人还不如我们研究下如何好好利用这个功能.

什么是未处理的 Rejection?

“Rejcetion” 是 Promise 表示错误状态的一个规范的说法. 在 ES6 的定义中, Promise 是一个含有三个状态的状态机: “pending”, “fulfilled” 和 “rejected”.

一个处于 pending 状态的 Promise 表示一个异步的操作正在执行当中; “fulfilled” 的表示异步的操作已经完成; “Rejected” 的表示异步操作因为某些原因已经失败了.比如: 我们用 MongoDB 驱动去连接一个不存在的 MongoDB 数据库, 那么我们就会到的一个 Promise 的 Rejection.

const { MongoClient } = require('mongodb');

MongoClient.connect('mongodb://notadomain');

/* Output:
$ node test.js
(node:9563) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): MongoError: failed to connect to server [notadomain:27017] on first connect [MongoError: getaddrinfo ENOTFOUND notadomain notadomain:27017]
(node:9563) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

ES6 的 Promise 的构造函数接受一个称为”执行器”的函数, 这个函数接受两个参数, resolve函数 和 reject函数. 调用 reject() 是可以使一个Promise 进如 rejection 状态, 这是第一种方法.

new Promise((resolve, reject) => {
  setTimeout(() => reject('woops'), 500);
});

/* Output:
$ node test.js
(node:8128) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): woops
(node:8128) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

另外一种方法就是直接在”执行器”函数中直接抛出异常.

new Promise(() => { throw new Error('exception!'); });

/* Output
$ node test.js
(node:8383) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: exception!
(node:8383) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

之前已经有一些讨论, 认为在执行器中直接抛出异常是一种糟糕的实践. 我表示强烈的反对. 健壮的错误处理是重要的设计模式目标, 如果不能直接在执行器中直接抛出异常, 而是采用类似 callback 时代中, 用 try/catch 包裹住回到异步函数来捕获异常来说是一种倒退.

then函数链式调用可以处理 Promise 处理的 Rejection. Promise 还提供了 .catch() 这个辅助方法直接来处理 Rejection.

new Promise((_, reject) => reject(new Error('woops'))).
  // Prints "caught woops"
  catch(error => { console.log('caught', error.message); });

// `.catch(fn)` 等效于 `.then(null, fn)`
new Promise((_, reject) => reject(new Error('woops'))).
  // Prints "caught woops"
  then(null, error => { console.log('caught', error.message); });

看到这里觉得 Promise 的错误处理很简单是吧? 看看下面的代码会打印什么.

new Promise((_, reject) => reject(new Error('woops'))).
  catch(error => { console.log('caught', err.message); });

它会打印 “UnhandledPromiseRejectionWarning”. 注意 err 是一个未定义的变量.

$ node test.js
(node:9825) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): ReferenceError: err is not defined
(node:9825) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

这就是为什么未处理的 Rejection 会很容易的隐藏在代码之中. 你认为你已经处理了 Promise 的异常, 而实际情况是你在处理的过程中在 .catch() 中引入了新的异常. 刚才那个例子还能看起来有点傻, 但是下面这个因为错误配置了 sentry, 在使用 sentry 来记录 Promise 错误的时候就会造成一个新的未处理的Rejection.

const sentry = require('raven');

new Promise((_, reject) => reject(new Error('woops'))).
  catch(error => new Promise((resolve, reject) => {
    sentry.captureMessage(error.message, function(error) {
      if (error) {
        return reject(error);
      }
      resolve();
    });
  }));

/* Output
$ node test.js
(node:10019) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3): TypeError: Cannot read property 'user' of undefined
(node:10019) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
*/

正是因为这种未处理的Rejection 非常的难查, node.js 引入了全局处理未处理的Rejection的机制.

unhandledRejection事件

node 进程使用unhandledRejection事件 用来处理未处理的 Reject 的 Promise,事件处理函数的第一个函数是Promise 的 reject 的错误. 需要注意的是这个事件只能用来处理node 原生的 Promise, 即使你用 global.Promise = require('bluebird') 来替换全局的 Promise 库也没用.

process.on('unhandledRejection', error => {
  // 打印 "unhandledRejection err is not defined"
  console.log('unhandledRejection', error.message);
});

new Promise((_, reject) => reject(new Error('woops'))).
  catch(error => {
    // 因为 err 未定义, 所以这个console.log 不会执行.
    console.log('caught', err.message);
  });

‘unhandledRejection’ 事件处理函数接收的 error 参数并没有强制要求必须是一个 Javascript 的 Error 对象. 虽然用一个非 Error 对象来调用 reject() 不是一个好习惯, 但是’unhandledRejection’的处理函数得到的参数就是 reject 的调用参数.

process.on('unhandledRejection', error => {
  // 打印 "unhandledRejection woops!"
  console.log('unhandledRejection', error.test);
});

new Promise((_, reject) => reject({ test: 'woops!' }));

注意你已经监听了 ‘unhandledRejection’, 默认输入到console的 UnhandledPromiseRejectionWarning 警告就不再打印. 这个信息只有在没有监听这个实践的时候打印. 如果你想临时的关闭警告, 用一个空函数调用 catch()即可

process.on('unhandledRejection', error => {
  // 不会打印 因为空函数catch了错误.
  console.log('unhandledRejection', error.test);
});

new Promise((_, reject) => reject({ test: 'woops!' })).catch(() => {});

当你完全确认你不需要处错误情况下, 你可以采用这样的方法来关闭未处理的Rejection. 有哪些情况是真正需要关闭错误处理的警告呢? 比如测试的当中, 我们需要 stub 一个函数, 让他返回一个指定的 Promise. 看下面的例子.

const sinon = require('sinon');

const obj = {
  fn: () => {}
};

before(function() {
  sinon.stub(obj, 'fn').returns(Promise.resolve());
});

it('works', function() {
  return obj.fn();
});

返回一个 Resolve 的 Promise的代码是完全没有问题的, 但是如果需要范围一个 Rejection 的 Promise呢, 看下面的代码, 它是会导致 未处理的 Rejection Promise 的问题的.

const assert = require('assert');
const sinon = require('sinon');

const obj = {
  fn: () => {}
};

before(function() {
  sinon.stub(obj, 'fn').returns(Promise.reject(new Error('test')));
});

it('works', function() {
  return obj.fn().catch(error => {
    assert.equal(error.message, 'test');
  });
});

在这种场景下 Promise 就打破了 Promise 的抽象边界. catch() 不再一个没有副作用的纯函数 (它的调用与否可能会影响全局的警告打印). 就上一个例子, 如果想不打印未处理啊的 Rejection 的警告, 就需要对stub返回的Promise先catch一次. (译者注: 同一个Promise是可以被then多次的,所以也可以被catch多次; 注意这里说的 then 多次不是指 then 的链式调用, then的链式调用的时候, 每then一次就会产生一个新的Promise)

before(function() {
  const p = Promise.reject(new Error('test'));
  p.catch(() => {});
  // 这样就没有 warning 了, 这个rejection 已经被处理过了.
  sinon.stub(obj, 'fn').returns(p);
});

Async/Awati的方案

Async/await 相对于自己手动采用then的链式调用的一大优点就是: await 会帮助帮你处理 catch()

async function test() {
  // 没有警告
  await Promise.reject(new Error('test'));
}

test().catch(() => {});

注意这里是因为 在 test() 函数之后已经 catch() 了, 所以没有打印警告. 如果没有 catch() 已经会有警告

async function test() {
  // 会打印警告
  await Promise.reject(new Error('test'));
}

test();

/* Output:
$ node test.js
(node:13912) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: test
(node:13912) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

Async/await 让你可以让 Promise 参与到一些复杂的流程控制中去, 比如循环,条件控制等等. 需要注意的是 Async 是会返回一个 Promise 的, 记得去 cacth()

还有一点就是, async 函数返回的是一个原生的 Promise, 你是无法替换成其他第三方的 Promise 的. 所以别想着替换原生 Promise 让Async逃脱出未处理的Rejection警告的魔爪.

global.Promise = require('bluebird');

async function test() {
  await Promise.reject(new Error('test'));
}

// 打印 "false"
console.log(test().catch(() => {}) instanceof require('bluebird'));

虽然你无法替换 async/await 中的 Promise 实现, 但是可以用 co 这样的黑科技来模拟 async/await 的操作. 顺便广告下: 如果你想自己从头写一个 co 库, 来加强对 async/await 的理解, 可以原文作者的书ES6 generators

分享到: QQ空间 新浪微博 腾讯微博 微信 更多
comments powered by Disqus