Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
295 views
in Technique[技术] by (71.8m points)

javascript - NodeJS Timeout a Promise if failed to complete in time

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!

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 continuing your myPromise function approach, perhaps something like this:

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

Used like this:

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

(Or possibly slightly less complicated, see under the divider below.)

I'd be slightly concerned about the fact that the semantics are different (no new, whereas you do use new with the Promise constructor). But the bigger issue with it is that it assumes you're always creating a promise from scratch, but you usually want to be able to use a promise you already have.

You can deal with both issues by subclassing 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));
    }
}

Usage (if constructing a new promise):

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

Usage (if using a promise you already have):

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

Live Example:

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...