NodeJS Timeout a Promise if failed to complete in time

AlexD picture AlexD · Sep 8, 2015 · Viewed 49k times · Source

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!

Answer

T.J. Crowder picture T.J. Crowder · Sep 8, 2015

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.