Search code examples
javascriptnode.jsbluebird

Passing arguments to a sequence of promises using bluebird .reduce


I have found a lot of promise reducing related questions but nothing that quite helps me with my particular problem so please excuse me in advance if I ended up missing the connection to my issue that might be obvious to someone else.

In short: I have a node script that needs to chain operations on a bunch of different files, these operations are async, wrapped in promises, but ultimately and for various reasons I'd like file processing to occur sequentially.

I ended up using the .reduce capabilities of Bluebird's promises in order to do so but I can't wrap my head around how I'm supposed to pass arguments to it all.

The following simplified example sums up the core of my issue :

const Promise = require('bluebird');

var f = (string) => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(string);
            resolve();
        }, 3000);
    })
    .catch(err => {
        console.error(err);
    });
};

var strings = ["hello", "world"];

strings.reduce((current, next) => {
    return f(current).then(f(next));
}, Promise.resolve([]))
.then(() => {
    console.log("All done!");
})
.catch(err => {
    console.error(err);
});

What I am expecting and what I need the script to do is for it to execute the promise over an array of arguments sequentially. I get the following output:

hello // after 3s
world // after 3s
All done! // after 3s

What I want is:

hello // after 3s
world // after 6s
All done! // after 6s

I understand that the problem is that promises are executed as soon as they're created and that I'm creating the second instance at the same time as the first one by using f(next).

I also know that using promises without parameters, i.e. a f1 with a hardcoded console.log("hello") and a f2 with a hardcoded console.log("world") works as intended when using

return f1.then(f2);

But I can't wrap my head around how to keep the async timings while at the same time passing parameters to it all. Thanks in advance for your time and help, I've been scratching my head over this for the past 24h.


Solution

  • You're making two calls to f on each iteration, where you only want one, and you want the one you make to wait for the previous promise to resolve — so you can't do .then(f(next)) because that doesn't wait, you need to pass in a function. (Also, since you never use the resolution value of the first promise, no need to give it an empty array.)

    So making both changes (one call, and passing in a function):

    strings.reduce((p, next) => {        // ***
        return p.then(() => f(next));    // *** Main changes here
    }, Promise.resolve())                // ***
    .then(() => {
        console.log("All done!");
    })
    .catch(err => {
        console.error(err);
    });
    

    Live Example (with a shorter timeout):

    var f = (string) => {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log(string);
                resolve();
            }, 1000);
        })
        .catch(err => {
            console.error(err);
        });
    };
    
    var strings = ["hello", "world"];
    
    strings.reduce((p, next) => {        // ***
        return p.then(() => f(next));    // *** Main changes here
    }, Promise.resolve([]))              // ***
    .then(() => {
        console.log("All done!");
    })
    .catch(err => {
        console.error(err);
    });

    I've also renamed the current parameter p, because it's not the "current" entry in strings, it's the previous value of the accumulator of reduce.


    Or with a concise arrow function:

    strings.reduce((p, next) => p.then(() => f(next)), Promise.resolve())
    .then(() => {
    // ...