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

问题描述 投票:0回答:8

如何在一定时间后使承诺超时? 我知道 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);
    });
}

谢谢!

javascript node.js promise settimeout
8个回答
89
投票

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

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

  1. 您没有提供实际执行承诺中任何操作的方法,并且

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

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

所以继续你的

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
,而您确实将
new
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) => {
    // ...
});

用法(如果使用您已有的承诺):

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

实例:

"use strict";
    
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));
    }
}

// Some functions for the demonstration
const neverSettle = () => new Promise(() => {});
const fulfillAfterDelay = (delay, value) => new Promise((resolve) => setTimeout(resolve, delay, value));
const rejectAfterDelay = (delay, error) => new Promise((resolve, reject) => setTimeout(reject, delay, error));

const examples = [
    function usageWhenCreatingNewPromise1() {
        console.log("Showing timeout when creating new promise");
        const p = new MyPromise(100, (resolve, reject) => {
            // We never resolve/reject, so we test the timeout
        });
        return p.then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    function usageWhenCreatingNewPromise2() {
        console.log("Showing when the promise is fulfilled before the timeout");
        const p = new MyPromise(100, (resolve, reject) => {
            setTimeout(resolve, 50, "worked");
        });
        return p.then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    function usageWhenCreatingNewPromise3() {
        console.log("Showing when the promise is rejected before the timeout");
        const p = new MyPromise(100, (resolve, reject) => {
            setTimeout(reject, 50, new Error("failed"));
        });
        return p.then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    function usageWhenYouAlreadyHaveAPromise1() {
        console.log("Showing timeout when using a promise we already have");
        return MyPromise.resolveWithTimeout(100, neverSettle())
        .then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    function usageWhenYouAlreadyHaveAPromise2() {
        console.log("Showing fulfillment when using a promise we already have");
        return MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"))
        .then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    function usageWhenYouAlreadyHaveAPromise3() {
        console.log("Showing rejection when using a promise we already have");
        return MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")))
        .then((value) => {
            console.log(`Fulfilled: ${value}`);
        })
        .catch((error) => {
            console.log(`Rejected: ${error}`);
        });
    },

    async function usageInAnAsyncFunction1() {
        console.log("Showing timeout in async function");
        try {
            const value = await MyPromise.resolveWithTimeout(100, neverSettle());
            console.log(`Fulfilled: ${value}`);
        } catch (error) {
            console.log(`Rejected: ${error}`);
        }
    },

    async function usageInAnAsyncFunction2() {
        console.log("Showing fulfillment in async function");
        try {
            const value = await MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"));
            console.log(`Fulfilled: ${value}`);
        } catch (error) {
            console.log(`Rejected: ${error}`);
        }
    },

    async function usageInAnAsyncFunction3() {
        console.log("Showing rejection in async function");
        try {
            const value = await MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")));
            console.log(`Fulfilled: ${value}`);
        } catch (error) {
            console.log(`Rejected: ${error}`);
        }
    },
];

(async () => {
    for (const example of examples) {
        try {
            await example();
        } catch (e) {
        }
    }
})();
/* Shows the cosole full height in the snippet */
.as-console-wrapper {
    max-height: 100% !important;
}


上面的代码在解决或拒绝 Promise 时主动取消计时器。根据您的用例,这可能不是必需的,并且会使代码有点复杂。对于事物的承诺部分来说,这不是必需的;一旦承诺被解决或拒绝,就无法更改,再次调用

resolve
reject
函数对承诺没有影响(规范对此很清楚)。但是,如果您不取消计时器,则计时器仍处于待处理状态,直到它触发为止。例如,待处理的承诺将阻止 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));
    }
}

56
投票

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

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

然后:

await withTimeout(5000, doSomethingAsync());

35
投票

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

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 个承诺,而不是 2 个。不过我认为这样更清楚。

您可能想要设置一个承诺,而不是让函数为您构建它。通过这种方式,您可以分离关注点,并最终专注于将您的 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.

10
投票

这是一个有点老的问题,但是当我寻找如何使承诺超时时,我偶然发现了这个问题。
虽然所有答案都很棒,但我发现使用 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/ });

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

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


4
投票

扩展方法是一个方便的解决方案:

fetch("/example")
    .withTimeout(5000);

这是通过向 Promise 全局对象prototype 添加一个方法来实现的。

promiseWithTimeout.js

/** Adds a timeout (in milliseconds) that will reject the promise when expired. */
Promise.prototype.withTimeout =
    function (milliseconds) {
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error("Timeout")), milliseconds);
            return this
                .then(value => {
                    clearTimeout(timeout);
                    resolve(value);
                })
                .catch(exception => {
                    clearTimeout(timeout);
                    reject(exception);
                });
        });
    };

export {}; // only needed if imported as a module

对于 TypeScript 支持,请在定义之前添加以下声明块

Promise.prototype.withTimeout

declare global {
    interface Promise<T> {
        /** Adds a timeout (in milliseconds) that will reject the promise when expired. */
        withTimeout(milliseconds: number): Promise<T>;
    }
}

2
投票

如果您的代码放置在类中,您可以使用装饰器。你在 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);


1
投票

虽然这里的答案是有效的,但您不应该尝试重新发明轮子,而应该使用 NPM 上数十个可用软件包之一来实现自我解决的承诺。

这是来自 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()
});

0
投票

在这种情况下包装纸会很方便

用法

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

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

甚至

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

实施

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

© www.soinside.com 2019 - 2024. All rights reserved.