Node.js Promise reject 用例调查

如今,Node.js 作为 OpenJS Foundation 中一个颇具影响力的项目, 通过向 stderr 发出弃用警告来处理还未被处理的 rejection。该警告显示了 rejection 发生在哪块内存栈中,并指出在将来的 Node.js 版本中,未处理的 rejection 将导致 Node.js 以非零状态码退出。我们打算删除弃用警告,将其替换为稳定的行为,该行为可能与弃用警告中描述的行为不同。我们正在进行一项调查,以更好地了解 Node.js 用户如何使用 Promises 以及如何处理未处理的 rejection,以至于在将来改动这一块的时候能作出明智的决定。

什么是 Promise rejection?

Promise rejection 表示执行 Promise 或 异步函数时出错了。在以下几种情况下可能会有 rejection:在异步函数或 Promise 执行程序/ then / catch / finally 回调中抛出错误,调用执行程序的 reject 回调或调用 Promise.reject

Promise.reject(new Error());  // This will result in a rejection

new Promise((fulfill, reject) => {
  reject(new Error());  // This will result in a rejection
});

new Promise(() => {
  throw new Error();  // This will result in a rejection
});

Promise.resolve().then(() => {
  throw new Error();  // This will result in a rejection
});

Promise.reject().then(() => {}, () => {
  throw new Error();  // This will result in a rejection
});

Promise.reject().catch(() => {
  throw new Error();  // This will result in a rejection
});

Promise.resolve().finally(() => {
  throw new Error();  // This will result in a rejection
});

async function foo() {
  throw new Error();  // This will result in a rejection
}

async function bar(a) {
  if (a === undefined)
    a();  // This will result in a rejection
}

在函数中添加 async 关键字会将该函数内部引发的任何异常(或从该函数内部调用的其他函数传播的所有异常)转化为 rejection。重构那些会引发异步函数/ Promises 的基于回调的代码时,也会发生同样的情况。下面的代码展示了这样的例子,当基于回调的代码被重构为 Promises 时,将异常变为 rejection:

// Callback version
const { readFile } = require('fs');

function readJsonFile(file, cb) {
  readFile(file, (err, data) => {
    if (err) {
      // If error while reading file, propagate the error via callback
      return cb(err, null);
    }
    // Unexpected invalid JSON input, code will throw
    cb(err, JSON.parse(data));
  });
}
// async function version
const { readFile } = require('fs').promises;

async function readJsonFile(file) {
  // Promise is rejected if fails to read or if unexpected JSON input.
  return JSON.parse(await readFile(file));
}

什么是未处理的 rejection?

有两种方法来处理 rejection:在后面添加.catch处理程序,或在 try / catch 块中执行 promise。在这两种情况下,rejection 的处理(传递给.catch的回调的执行,或catch {}中代码块的执行)都将在时间循环中异步执行。

Promise 的设计使得从创建 Promise 时(可能在 pending 状态的时候)到程序完成执行之前的任何时间点,都可以添加处理程序或等待。

从 rejection 发生到将处理程序附加到 Promise 或在 catch {} 块中等待 Promise 的那一刻,rejection 就被视为未处理的。以下是一些已处理和未处理 rejection 的示例。

async function foo() {
  throw new Error();
}

foo()  // 1. Unhandled at this point
  .catch(() => console.error("an error occured"));  // 2. Now it's handled

try {
  await foo();
} catch(e) {  // 3. Handled
  console.error("an error occured");
}

foo(); // 4. Unhandled, but execution continues

const rejected = foo(); // 5. Unhandled on current event loop turn, but handled
                        // in a future turn of the event loop when the
                        // setTimeout callback below is executed.
setTimeout(() => rejected.catch(() => console.error("an error occured")), 100);

正如我们在示例中看到的那样,将来可能会处理未处理的 rejection,例如示例5,但它也可能永远保持未处理状态(例如示例4)。

某些未处理的 rejection 在极少数情况下可能会使您的应用程序处于不确定性和不安全状态,无论是内部应用程序状态(包括内存泄漏)、应用程序使用的外部资源(例如文件句柄或数据库连接)还是外部状态( 例如,数据库中数据的一致性)。例如,以下服务器未将响应发送回客户端,从而导致套接字泄漏,并且可能会导致拒绝服务攻击:

const http = require('http');
const server = http.createServer(handle);

server.listen(3000);

function handle (req, res) {
  doStuff()
    .then((body) => {
      res.end(body);
    });
}

function doStuff () {
  if (Math.random() < 0.5) {
    return Promise.reject(new Error('kaboom'));
  }

  return Promise.resolve('hello world');
}

解决未处理的 rejection

开发人员可以通过几种方法来解决未处理的 rejection。Node.js 提供了一个未处理的 rejection 侦听器(process.on('unhandledRejection')),开发人员可以使用该侦听器以编程方式确定 rejection 是否有可能会泄漏资源或使应用程序处于不确定状态。使用上面的服务器示例, 开发人员可以通过以下方法缓解此问题:

const http = require('http');
const server = http.createServer(handle);

server.listen(3000);

process.on('unhandledRejection', (err) => {
  throw err;
});

function handle (req, res) {
  doStuff()
    .then((body) => {
      res.end(body);
    });
}

function doStuff () {
  if (Math.random() < 0.5) {
    return Promise.reject(new Error('kaboom'));
  }

  return Promise.resolve('hello world');
}

该进程会崩溃,但是在这种情况下,这可能比泄漏资源更好。在其他情况下,侦听器可能会尝试通过先响应仍在处理中的其他请求来正常恢复或关闭,并且仅在无法响应未决请求时才崩溃。

使用侦听器需要更改应用程序代码,并且该行为可以被任何依赖项覆盖。为了避免这些限制,向 Node.js 添加了一个新标志(--unhandled-rejection = [mode])。该标志支持五种不同的模式:

  • strict:引发未捕获的异常,类似于没有捕获错误时 throw new Error()unhandledRejection 侦听器不会阻止引发异常。
  • throw:引发未捕获的异常,类似于没有捕获错误时 throw new Error()unhandledRejection 侦听器具有优先权并防止引发异常。
  • warn:尽快输出警告。发出警告后继续运行。如果该进程退出并且未设置任何状态代码,则该进程以成功代码退出。这类似于浏览器控制台的功能。
  • warn-with-error-code:尽快输出警告。发出警告后继续运行。如果该过程退出并且未设置任何状态代码,则该过程将退出并显示错误代码。
  • none:不做任何处理。

对于所有模式,这些操作(引发异常输出警告)将在 nextTick 上发生。strict 模式下可以避免上述问题,因为它可以防止依赖项覆盖 unhandleRejection 侦听器。warn 模式类似于当前行为,但没有弃用警告。

可以使用侦听器和标志来解决未处理的 rejection。

为该项目定义默认行为

如上所述,未处理的 rejection 可能会泄漏资源,但它们也可能是无害的,甚至有时候需要这些未处理的 rejection。因此,过去几年来,该项目中一直在讨论默认行为应该是什么这是一个两极分化的问题,因此到目前为止尚未达成共识。 TSC将在几周内对该问题进行投票,但是在我们投票之前,我们想更好地了解开发人员今天如何在 Node.js 上使用 Promises 以及他们认为默认值应该是什么。

为了帮助我们确定默认值,请完成我们的调查:https://www.surveymonkey.com/r/FTJM7YD

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值