Search code examples
javascriptpromisees6-promise

Wait for all nested promises to complete, but still react to each individual resolve


Suppose that newsService.getNews() returns a promise that should resolve to a random news entry returned by some service, while translateService.translate() returns a promise that should resolve to the translation of the passed text.

var newsPromises = [];
var translatePromises = [];
for (var i = 0; i < 5; i++) {
    var p1 = this.newsService.getNews();
    newsPromises.push(p1);

    p1.then(function (data) {
        var p2 = this.translateService.translate(data);
        translatePromises.push(p2);
        p2.then(function (translatedData) {
            addNews(`${data} (${translatedData})`);

        }, function (fail) {
            console.log(fail.message);
        });

    }, function (fail) {
        console.log(fail.message);
    });
}

now the page initially shows a loading spinner that I would like to hide when all the promises (including the nested translation promises) have completed (succeeded or failed):

   Promise.all(newsPromises)
        .then(function (results) {
            Promise.all(translatePromises).then(function (results) {
                    removeLoading();
                },
                function (err) {
                    removeLoading();
                }
            );

        }, function (err) {
            Promise.all(translatePromises).then(function (results) {
                    removeLoading();
                },
                function (err) {
                    removeLoading();
                }
            );
        });

This code a) does not work as it should, since the loading spinner some times disappears before the promises resolve, and b) is horribly complex.

How is this done properly? (with vanilla JS / ES6)


Solution

  • Remember that promises chains are pipelines, where each handler can transform the chain's result as the result passes through the handler. See comments:

    // We only need one array of promises
    const promises = [];
    // Build the array
    for (let i = 0; i < 5; i++) {
        // Add this promise to the array
        promises.push(
            // Get the news...
            this.newsService.getNews().then(
                // ...and translate it...
                data => this.translateService.translate(data)
                    .then(translatedData => {
                        // ...and show it as soon as it's available
                        addNews(`${data} (${translatedData})`);
                        // Note that here we're converting the resolution value to
                        // `undefined`, but nothing uses it so...
                        // If you want something to be able to use it,
                        // return `translatedData` (or `data` or...)
                    })
            )
            .catch(fail => {
                console.log(fail.message);
                // WARNING: Here you're converting rejection to resolution with `undefined`
            })
        );
    }
    // Wait until all that is done before removing the loading indicator
    Promise.all(promises).then(removeLoading);
    

    Note that the only reason we don't need a catch on the Promise.all promise is that you're ignoring (other than logging) errors that occur, so we know that promise will never reject.

    Also note that the above assumes removeLoading doesn't pay any attention to the arguments it receives, and that it doesn't return a promise that may reject. If it does care about arguments and it's important to call it with no arguments, change the Promise.all bit to:

    Promise.all(promises).then(() => removeLoading());
    

    If it returns a promise that may reject, you'll need a catch handler as well.