如果未能及时完成,NodeJS 超时Promise

IT技术 javascript node.js promise settimeout
2021-01-30 08:20:58

如何在一定时间后超时Promise?我知道 Q 有一个Promise超时,但我使用的是原生 NodeJS Promise,他们没有 .timeout 功能。

我错过了一个还是它的包装方式不同?

或者,下面的实现在不占用内存方面是否很好,实际上按预期工作?

我也可以让它以某种方式全局包装,以便我可以将它用于我创建的每个Promise,而不必重复 setTimeout 和 clearTimeout 代码?

function run() {
    logger.info('DoNothingController working on process id {0}...'.format(process.pid));

    myPromise(4000)
        .then(function() {
            logger.info('Successful!');
        })
        .catch(function(error) {
            logger.error('Failed! ' + error);
        });
}

function myPromise(ms) {
    return new Promise(function(resolve, reject) {
        var hasValueReturned;
        var promiseTimeout = setTimeout(function() {
            if (!hasValueReturned) {
                reject('Promise timed out after ' + ms + ' ms');
            }
        }, ms);

        // Do something, for example for testing purposes
        setTimeout(function() {
            resolve();
            clearTimeout(promiseTimeout);
        }, ms - 2000);
    });
}

谢谢!

6个回答

原生 JavaScript Promise没有任何超时机制。

关于您的实现的问题可能更适合http://codereview.stackexchange.com,但有几个注意事项:

  1. 你没有提供在Promise中实际做任何事情的方法,并且

  2. clearTimeout在您的setTimeout回调中不需要,因为setTimeout安排了一次性计时器。

  3. 由于Promise一旦被解决/拒绝就无法解决/拒绝,因此您不需要该检查。

所以继续你的myPromise函数方法,也许是这样的:

function myPromise(timeout, callback) {
    return new Promise((resolve, reject) => {
        // Set up the timeout
        const timer = setTimeout(() => {
            reject(new Error(`Promise timed out after ${timeout} ms`));
        }, timeout);

        // Set up the real work
        callback(
            (value) => {
                clearTimeout(timer);
                resolve(value);
            },
            (error) => {
                clearTimeout(timer);
                reject(error);
            }
        );
    });
}

像这样使用:

myPromise(2000, (resolve, reject) => {
    // Real work is here
});

(或者可能稍微不那么复杂,请参见下面的分隔符。)

我会稍微担心语义不同的事实(不new,而您确实newPromise构造函数一起使用)。但更大的问题是它假设你总是从头开始创建一个Promise,但你通常希望能够使用你已经拥有的Promise。

您可以通过子类化来处理这两个问题Promise

class MyPromise extends Promise {
    constructor(timeout, callback) {
        // We need to support being called with no milliseconds
        // value, because the various Promise methods (`then` and
        // such) correctly call the subclass constructor when
        // building the new promises they return.
        const haveTimeout = typeof timeout === "number";
        const init = haveTimeout ? callback : timeout;
        super((resolve, reject) => {
            if (haveTimeout) {
                const timer = setTimeout(() => {
                    reject(new Error(`Promise timed out after ${timeout}ms`));
                }, timeout);
                init(
                    (value) => {
                        clearTimeout(timer);
                        resolve(value);
                    },
                    (error) => {
                        clearTimeout(timer);
                        reject(error);
                    }
                );
            } else {
                init(resolve, reject);
            }
        });
    }
    // Pick your own name of course. (You could even override `resolve` itself
    // if you liked; just be sure to do the same arguments detection we do
    // above in the constructor, since you need to support the standard use of
    // `resolve`.)
    static resolveWithTimeout(timeout, x) {
        if (!x || typeof x.then !== "function") {
            // `x` isn't a thenable, no need for the timeout,
            // fulfill immediately
            return this.resolve(x);
        }
        return new this(timeout, x.then.bind(x));
    }
}

用法(如果构建一个新的Promise):

let p = new MyPromise(300, (resolve, reject) => {
    // ...
});
p.then((value) => {
    // ...
})
.catch((error) => {
    // ...
});

用法(如果使用您已经拥有的Promise):

MyPromise.resolveWithTimeout(100, somePromiseYouAlreadyHave)
.then((value) => {
    // ...
})
.catch((error) => {
    // ...
});

现场示例:


上面的代码在解决或拒绝Promise时主动取消计时器。根据您的用例,这可能不是必需的,并且会使代码复杂化。事情的 promise 部分没有必要;一旦Promise被解决或拒绝,不能改变,再次调用resolvereject函数对Promise没有影响(规范对此很清楚)。但是,如果您不取消计时器,则计时器在触发之前仍处于挂起状态。例如,一个挂起的Promise会阻止 Node.js 退出,所以如果你在做的最后一件事上超时很长时间,它可能会毫无意义地延迟退出进程。浏览器不会使用挂起的计时器延迟离开页面,因此这不适用于浏览器。同样,您的里程可能会有所不同,您可以通过不取消计时器来简化一些。

如果你不关心挂起的计时器,MyPromise会更简单:

class MyPromise extends Promise {
    constructor(timeout, callback) {
        // We need to support being called with no milliseconds
        // value, because the various Promise methods (`then` and
        // such) correctly call the subclass constructor when
        // building the new promises they return.
        const haveTimeout = typeof timeout === "number";
        const init = haveTimeout ? callback : timeout;
        super((resolve, reject) => {
            init(resolve, reject);
            if (haveTimeout) {
                setTimeout(() => {
                    reject(new Error(`Promise timed out after ${timeout}ms`));
                }, timeout);
            }
        });
    }
    // Pick your own name of course. (You could even override `resolve` itself
    // if you liked; just be sure to do the same arguments detection we do
    // above in the constructor, since you need to support the standard use of
    // `resolve`.)
    static resolveWithTimeout(timeout, x) {
        if (!x || typeof x.then !== "function") {
            // `x` isn't a thenable, no need for the timeout,
            // fulfill immediately
            return this.resolve(x);
        }
        return new this(timeout, x.then.bind(x));
    }
}
@MikaelFinstad - 感谢您的评论!我很惊讶我按照自己的方式写答案,这通常会困扰我。我已经更新了答案(还有其他要更改/更新的内容......)。
2021-03-18 08:20:58
实际上,如果 Promise 解决,取消超时仍然很好,因为任何等待的超时都会导致 Node.js 挂起,直到所有超时都被触发,退出时。
2021-04-07 08:20:58

虽然可能不支持Promise超时,但您可以竞争Promise:

var race = Promise.race([
  new Promise(function(resolve){
    setTimeout(function() { resolve('I did it'); }, 1000);
  }),
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, 800);
  })
]);

race.then(function(data){
  console.log(data);
  }).catch(function(e){
  console.log(e);
  });

一个通用的Promise.timeout

Promise.timeout = function(timeout, cb){
  return Promise.race([
  new Promise(cb),
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, timeout);
  })
]);
}

例子:

    Promise.timeout = function(timeout, cb) {
      return Promise.race([
        new Promise(cb),
        new Promise(function(resolve, reject) {
          setTimeout(function() {
            reject('Timed out');
          }, timeout);
        })
      ]);
    }
    
    function delayedHello(cb){
      setTimeout(function(){
        cb('Hello');
        }, 1000);
      }
    
    Promise.timeout(800, delayedHello).then(function(data){
      console.log(data);
      }).catch(function(e){
      console.log(e);
      }); //delayedHello doesn't make it.

    Promise.timeout(1200, delayedHello).then(function(data){
      console.log(data);
      }).catch(function(e){
      console.log(e);
      }); //delayedHello makes it.

可能有点贵,因为您实际上是在创建 3 个Promise而不是 2 个。我认为这样更清楚。

您可能想要设置一个 promise,而不是让函数为您构造它。通过这种方式,您可以分离关注点,并且最终专注于将您的Promise与新构建的Promise进行竞争,该Promise将在x几毫秒内拒绝

Promise.timeout = function(timeout, promise){
  return Promise.race([
  promise,
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, timeout);
  })
]);
}

如何使用:

var p = new Promise(function(resolve, reject){
    setTimeout(function() { resolve('Hello'); }, 1000);
});

Promise.timeout(800, p); //will be rejected, as the promise takes at least 1 sec.
谢谢@Bergi,我不知道为什么要嵌套回调。我实际上正在考虑为第二个参数接受不同的输入,回调函数传递给Promise构造函数或 aPromise或数组,Promises但我觉得答案的范围仅限于回调函数,我不想强​​加代码中的混淆。不过,我会添加您的建议。
2021-03-18 08:20:58
我建议让我们timeout直接接受Promise,而不是只传递给Promise构造函数的回调它可能会鼓励 promise 构造函数反模式。
2021-03-22 08:20:58
您可以将其缩短为……new Promise(cb) …
2021-04-13 08:20:58

要将超时添加到任何现有的Promise,您可以使用:

const withTimeout = (millis, promise) => {
    const timeout = new Promise((resolve, reject) =>
        setTimeout(
            () => reject(`Timed out after ${millis} ms.`),
            millis));
    return Promise.race([
        promise,
        timeout
    ]);
};

后来:

await withTimeout(5000, doSomethingAsync());
你节省了 5 个该死的星期。谢谢!
2021-04-09 08:20:58

这是一个有点老的问题,但是当我在寻找如何使Promise超时时偶然发现了这个问题。
虽然所有答案都很好,但我发现使用Promises 的bluebird实现是处理超时的最简单方法

var Promise = require('bluebird');
var p = new Promise(function(reject, resolve) { /.../ });
p.timeout(3000) //make the promise timeout after 3000 milliseconds
 .then(function(data) { /handle resolved promise/ })
 .catch(Promise.TimeoutError, function(error) { /handle timeout error/ })
 .catch(function(error) { /handle any other non-timeout errors/ });

如您所见,这比其他建议的解决方案工作量少得多。我想我会把它放在这里让人们更容易找到它:)

顺便说一句,我绝不参与 bluebird 项目,只是发现这个特定的解决方案非常简洁。

我最喜欢这个解决方案,因为它展示了如何分别处理超时和其他错误!
2021-03-27 08:20:58

如果您的代码放在一个类中,您可以为此使用装饰器。您在utils-decorators 库( npm install --save utils-decorators) 中有这样的装饰

import {timeout} from 'utils-decorators';

class SomeService {

   @timeout(3000)
   doSomeAsync(): Promise<any> {
    ....
   }
}

或者您可以使用包装函数:

import {timeoutify} from 'utils-decorators';

const withTimeout = timeoutify(originalFunc, 3000);