NodeJS Timeout a Promise if failed to complete in time

前端 未结 7 506
忘掉有多难
忘掉有多难 2020-11-27 04:07

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.

相关标签:
7条回答
  • 2020-11-27 04:21

    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.
    
    0 讨论(0)
  • 2020-11-27 04:22

    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());
    
    0 讨论(0)
  • 2020-11-27 04:25

    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.

    0 讨论(0)
  • 2020-11-27 04:27

    If your code is placed in a class you could use a decorator for that. You have such decorator in the utils-decorators (npm install --save utils-decorators):

    import {timeout} from 'utils-decorators';
    
    class SomeService {
    
       @timeout(3000)
       doSomeAsync(): Promise<any> {
        ....
       }
    }
    

    https://github.com/vlio20/utils-decorators#timeout-method

    0 讨论(0)
  • 2020-11-27 04:31

    A wrapper would be convenient in this situation

    usage

    const result = await withTimeout(() => doSomethingAsync(...args), 3000)();
    

    or

    const result = await withTimeout(doSomethingAsync, 3000)(...args);
    

    or even

    const doSomethingAsyncWithTimeout = withTimeout(doSomethingAsync, 3000);
    const result = await doSomethingAsyncWithTimeout(...args);
    

    implementation

    /**
     * returns a new function which calls the input function and "races" the result against a promise that throws an error on timeout.
     *
     * the result is:
     * - if your async fn takes longer than timeout ms, then an error will be thrown
     * - if your async fn executes faster than timeout ms, you'll get the normal response of the fn
     *
     * ### usage
     * ```ts
     * const result = await withTimeout(() => doSomethingAsync(...args), 3000);
     * ```
     * or
     * ```ts
     * const result = await withTimeout(doSomethingAsync, 3000)(...args);
     * ```
     * or even
     * ```ts
     * const doSomethingAsyncWithTimeout = withTimeout(doSomethingAsync, 3000);
     * const result = await doSomethingAsyncWithTimeout(...args);
     * ```
     */
    const withTimeout = <R, P extends any, T extends (...args: P[]) => Promise<R>>(logic: T, ms: number) => {
      return (...args: Parameters<T>) => {
        // create a promise that rejects in <ms> milliseconds; https://italonascimento.github.io/applying-a-timeout-to-your-promises/
        const timeout = new Promise((resolve, reject) => {
          const id = setTimeout(() => {
            clearTimeout(id);
            reject(new Error(`promise was timed out in ${ms} ms, by withTimeout`));
          }, ms); // tslint:disable-line align
        });
    
        // returns a "race" between our timeout and the function executed with the input params
        return Promise.race([
          logic(...args), // the wrapped fn, executed w/ the input params
          timeout, // the timeout
        ]) as Promise<R>;
      };
    };
    
    
    0 讨论(0)
  • 2020-11-27 04:37

    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.

    0 讨论(0)
提交回复
热议问题