Stop memory leaks with recursive promises

engie picture engie · Feb 22, 2013 · Viewed 7.6k times · Source

How do I create a recursive chain of JavaScript Promises with the Q library? The following code fails to complete in Chrome:

<html>
    <script src="q.js" type="text/javascript"></script>
    <script type="text/javascript">
        //Don't keep track of a promises stack for debugging
        //Reduces memory usage when recursing promises
        Q.longStackJumpLimit = 0;

        function do_stuff(count) {
            if (count==1000000) {
                return;
            }

            if (count%10000 == 0){
                console.log( count );
            }

            return Q.delay(1).then(function() {
                return do_stuff(count+1);
            });
        }

        do_stuff(0)
        .then(function() {
            console.log("Done");
        });
    </script>
</html>

Answer

ForbesLindesay picture ForbesLindesay · Feb 23, 2013

This won't stack overflow because promises break the stack, but it will leak memory. If you run this same code in node.js you'll get an error that reads:

FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory

What is happening here is that a really long chain of nested promises is being created, each waiting for the next. What you need to do is find a way to flatten that chain so that there is just one top level promise that gets returned, waiting on the inner most promise that is currently representing some real work.

breaking the chain

The easiest solution is to construct a new promise at the top level and use it to break the recursion:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    return new Promise(function (resolve, reject) {
        function doStuffRecursion(count) {
            if (count==1000000) {
                return resolve();
            }

            if (count%10000 == 0){
                console.log( count );
            }

            delay(1).then(function() {
                doStuffRecursion(count+1);
            }).done(null, reject);
        }
        doStuffRecursion(count);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Although this solution is somewhat inelegant, you can be sure it will work in all promise implementations.

then/promise now supports tail recursion

Some promise implementations (for example promise from npm, which you can download as a standalone library from https://www.promisejs.org/) correctly detect this case and collapse the chain of promises into a single promise. This works providing you don't keep a reference to the promise returned by the top level function (i.e. call .then on it immediately, don't keep it around).

Good:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Bad:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

var thisReferenceWillPreventGarbageCollection = do_stuff(0);

thisReferenceWillPreventGarbageCollection.then(function() {
    console.log("Done");
});

Unfortunately, none of the built in promise implementations have this optimisation, and none have any plans to implement it.