Search code examples
javascriptes6-promiserace-condition

Promise Chaining Race-Condition


I'm currently working on a rather simple logic for processing queued ZPL print jobs, which are stored in an array that's then being iterated sending n amount of copies per job to a printer.

I'm reducing the array into a promise chain mixing-in a sub-chain for each job which sends the copies to the printer. The calls to the printer are synchronous (ye I know...) so I wrapped each one of them into a Promise that only resolves when the printer received the copy, thus ensuring sequential processing.

In case of a failed transmission the current promise rejects with a hand-crafted error which is being caught in the main-chain.

So far the theory, alas there seems to be a kind of race-condition between the sub-chains.

I tried my best, but I simply don't see it...

Here some simplified code + fiddle, notice how the sub-chains are not running subsequential:

['job1', 'job2'].reduce((pMain, item, curIndex) => {

    var pSub = Promise.resolve();

    for (let i = 0; i < 2; i++) pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));

    return pMain.then(() => pSub);

}, Promise.resolve())
.then(() => /* print all done */)
.catch( handleError );

jsfiddle with console.logs here

Any advice is highly appreciated. Being stuck at something so trivial is a bummer.


Solution

  • Your pSub chains are all created and run synchronously during the reduce call. To become sequential, they need to go inside the then callback:

    ['job1', 'job2'].reduce((pMain, item, curIndex) => {
        return pMain.then(() => {
            var pSub = Promise.resolve();
            for (let i = 0; i < 2; i++)
                pSub = pSub.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
            return pSub;
        });
    }, Promise.resolve())
    

    Alternatively build only a single chain across the two loops:

    ['job1', 'job2'].reduce((promise, item, outerIndex) => {
        return Array.from({length: 2}).reduce((promise, _, innerIndex) => {
            return promise.then(() => new Promise((resolve, reject) => setTimeout(reject, 2000)));
        }, promise);
    }, Promise.resolve())
    

    Of course @jfriend is right, for sequential tasks you should just write async/await code:

    for (const item of ['job1', 'job2']) {
        for (let i = 0; i < 2; i++) {
            await new Promise((resolve, reject) => setTimeout(reject, 2000));
        }
    }
    

    You can also easily put a try block on the right level with that solution.