NodeJS Timeout a Promise if failed to complete in time

我们两清 提交于 2019-12-17 09:18:24

问题


How can I timeout a promise after certain amount of time? I know Q has a promise timeout, but I'm using native NodeJS promises and they don't have .timeout function.

Am I missing one or its wrapped differently?

Alternatively, Is the below implementation good in means of not sucking up memory, actually working as expected?

Also can I make it somehow wrapped globally so I can use it for every promise I create, without having to repeat the setTimeout and clearTimeout code?

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);
    });
}

Thanks!


回答1:


Native JavaScript promises don't have any timeout mechanism.

The question about your implementation would probably be a better fit for http://codereview.stackexchange.com, but a couple of notes:

  1. You don't provide a means of actually doing anything in the promise, and

  2. There's no need for clearTimeout within your setTimeout callback, since setTimeout schedules a one-off timer.

  3. Since a promise can't be resolved/rejected once it's been resolved/rejected, you don't need that check.

So perhaps something along these lines:

function myPromise(ms, callback) {
    return new Promise(function(resolve, reject) {
        // Set up the real work
        callback(resolve, reject);

        // Set up the timeout
        setTimeout(function() {
            reject('Promise timed out after ' + ms + ' ms');
        }, ms);
    });
}

Used like this:

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

(Or you may want it to be a bit more complicated, see update under the line below.)

I'd be slightly concerned about the fact that the semantics are slightly different (no new, whereas you do use new with the Promise constructor), so you might adjust that.

The other problem, of course, is that most of the time, you don't want to construct new promises, and so couldn't use the above. Most of the time, you have a promise already (the result of a previous then call, etc.). But for situations where you're really constructing a new promise, you could use something like the above.

You can deal with the new thing by subclassing Promise:

class MyPromise extends Promise {
    constructor(ms, 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.
        // This code to do it is ugly, could use some love, but it
        // gives you the idea.
        let haveTimeout = typeof ms === "number" && typeof callback === "function";
        let init = haveTimeout ? callback : ms;
        super((resolve, reject) => {
            init(resolve, reject);
            if (haveTimeout) {
                setTimeout(() => {
                    reject("Timed out");
                }, ms);
            }
        });
    }
}

Usage:

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

Live Example:

// Uses var instead of let and non-arrow functions to try to be
// compatible with browsers that aren't quite fully ES6 yet, but
// do have promises...
(function() {
    "use strict";
    
    class MyPromise extends Promise {
        constructor(ms, callback) {
            var haveTimeout = typeof ms === "number" && typeof callback === "function";
            var init = haveTimeout ? callback : ms;
            super(function(resolve, reject) {
                init(resolve, reject);
                if (haveTimeout) {
        	        setTimeout(function() {
    	                reject("Timed out");
	                }, ms);
                }
            });
        }
    }
    
    var p = new MyPromise(100, function(resolve, reject) {
        // We never resolve/reject, so we test the timeout
    });
    p.then(function(result) {
    	snippet.log("Resolved: " + result);
    }).catch(function(reject) {
        snippet.log("Rejected: " + reject);
    });
})();
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Both of those will call reject when the timer expires even if the callback calls resolve or reject first. That's fine, a promise's settled state cannot be changed once it's set, and the spec defines calls to resolve or reject on a promise that's already settled as do-nothings that don't raise an error.

But if it bother you, you could wrap resolve and reject. Here's myPromise done that way:

function myPromise(ms, callback) {
    return new Promise(function(resolve, reject) {
        // Set up the timeout
        let timer = setTimeout(function() {
            reject('Promise timed out after ' + ms + ' ms');
        }, ms);
        let cancelTimer = _ => {
            if (timer) {
                clearTimeout(timer);
                timer = 0;
            }
        };

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

You can spin that about 18 different ways, but the basic concept is that the resolve and reject we pass the promise executor we receive are wrappers that clear the timer.

But, that creates functions and extra function calls that you don't need. The spec is clear about what the resolving functions do when the promise is already resolved; they quit quite early.




回答2:


While maybe there's no support for a promise timeout, you could race promises:

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);
  });

A generic Promise.timeout:

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

Example:

    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.

Might be a little bit costly, because you are actually creating 3 promises instead of 2. I think it's clearer this way though.

You might want to setup a promise instead of having the function construct it for you. This way you separate concerns and you are ultimately focused on racing your promise against a newly built promise that will reject at x miliseconds.

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

How to use:

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.



回答3:


This is slightly old question, but I stumbled upon this when I was looking how to timeout a promise.
Whilst all the answers are great, I found using bluebird implementation of Promises as the easiest way of handling timeouts:

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/ });

As you can see this is so much less work than the other proposed solutions. I thought I will put it here to make it easier for people to find it :)

Btw I am not by any means involved in bluebird project, just found this particular solution very neat.




回答4:


To add a timeout to any existing promise, you can use:

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

Then later:

await withTimeout(5000, doSomethingAsync());



回答5:


While the answers here are valid, you should not try to reinvent the wheel and just use one of the dozens available packages on NPM for the self-resolving promise.

Here's one example from NPM:

const { TimeoutResolvePromise, TimeoutRejectPromise } = require('nodejs-promise-timeout');
const TIMEOUT_DELAY = 2000;

// This promise will reject after 2 seconds:
let promise1 = new TimeoutRejectPromise(TIMEOUT_DELAY, (resolve, reject) => {
  // Do something useful here, then call resolve() or reject()
});


来源:https://stackoverflow.com/questions/32461271/nodejs-timeout-a-promise-if-failed-to-complete-in-time

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!