Friday, February 3, 2017

Calling promises in a sequence

One of the top questions about promises I was asked lately is a question on how to call promises in a sequence so that the next starts only after the previous one completes.

The question is asked mostly by people with C# background where a Task has to be started in an explicit way. In contrast, in Javascript, as soon as you have the promise object, it the execution has already started. Thus, following naive approach doesn’t work:

function getPromise(n) {
    return new Promise( (res, rej) => {
        setTimeout( () => {
            console.log(n);
            res(n);
        }, 1000 );
    });
}

console.log('started');

// in parallel
Promise.all( [1,2,3,4,5].map( n => getPromise(n)) )
    .then( result => {
        console.log('finished with', result);
    });

The above code yields 1,2,3,4,5 as soon as 1 second passes and there are no further delays, although people expect them here. The problem with this approach is that as soon as getPromise is called, the promise code starts. There are then 5 timers in total, started in almost the very same time, the time it takes the map function to loop over the source array.

In order to call promises in a sequence we have to completely rethink the approach. Each next promise should be invoked from within the then handler of the previous promise as then is called only where a promise is fulfilled. A simple structural trick is to use map.reduce in order to loop over the array so that each time we have the access to the previous “value” (the previous promise) and the current value (current number/whatever from the array). The map.reduce needs an “initial value” and it should be a promise so we just feed it with a dummy, resolved promise.

function getPromise(n) {
    return new Promise( (res, rej) => {
        setTimeout( () => {
            console.log(n);
            res(n);
        }, 1000 );
    });
}

console.log('started');

// in a sequence
[1,2,3,4,5].reduce( (prev, cur) => {
    return prev.then( result => {
        return getPromise(cur);
    });
}, Promise.resolve() )
    .then( result => {
        console.log('finished with', result);
    });

A neat trick worth of remembering.