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

NodeJS Timeout a Promise if failed to complete in time

如何在一定时间后使承诺超时? 我知道 Q 有一个 promise 超时,但我使用的是原生 NodeJS promises,它们没有 .timeout 函数。

我是漏了一个还是包装方式不同?

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

我还能以某种方式将它全局包装,这样我就可以将它用于我创建的每个承诺,而不必重复 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 承诺没有任何超时机制。

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

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

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

  3. 既然一个承诺一旦 resolved/rejected 就不能 resolved/rejected,你不需要那个检查。

所以继续你的 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:

来解决这两个问题
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));
    }
}

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

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


上面的代码在解决或拒绝承诺时主动取消计时器。根据您的用例,这可能不是必需的,并且会使代码稍微复杂一些。事情的承诺部分没有必要;一旦承诺被解决或拒绝,就不能更改,再次调用 resolvereject 函数对承诺没有影响(the spec 对此很清楚)。但是,如果您不取消计时器,则计时器在触发之前仍处于等待状态。例如,挂起的 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));
    }
}

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

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 个。不过我认为这样更清楚。

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

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

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

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

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

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

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

这是一个 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()
});

如果您的代码放在 class 中,您可以为此使用装饰器。你在 utils-decorators library (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);

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

用法

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

Extension Method 是一个方便的解决方案:

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

这是通过在prototype of the Promise global object中添加一个方法来实现的。

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