37

I have several promises that I need to resolve before going further.

Promise.all(promises).then((results) => {
  // going further
}); 

Is there any way I can have the progress of the Promise.all promise?

From the doc, it appears that it is not possible. And this question doesn't answer it either.

So:

  • Don't you agree that this would be useful? Shouldn't we query for this feature?
  • How can one implement it manually for now?
Community
  • 1
  • 1
Augustin Riedinger
  • 20,909
  • 29
  • 133
  • 206
  • You can always have the length of the `promises` array and from each promise callback increment a shared variable value using some function like `incrementCount()` and on the same object create a function like `getPercent()` that returns `counter*100/promises.length` as each resolve or reject happens per promise. – ishaan Feb 20 '17 at 09:59
  • 1
    You can simulate promise.progress() by adding a `.then()` to each promise before you push them to the array you will `Promise.all()`. It's a little bit of extra overhead, but can be handy. – Shilly Feb 20 '17 at 10:06

7 Answers7

65

I've knocked up a little helper function that you can re-use.

Basically pass your promises as normal, and provide a callback to do what you want with the progress..

function allProgress(proms, progress_cb) {
  let d = 0;
  progress_cb(0);
  for (const p of proms) {
    p.then(()=> {    
      d ++;
      progress_cb( (d * 100) / proms.length );
    });
  }
  return Promise.all(proms);
}

function test(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
       console.log(`Waited ${ms}`);
       resolve();
     }, ms);
  });
}


allProgress([test(1000), test(3000), test(2000), test(3500)],
  (p) => {
     console.log(`% Done = ${p.toFixed(2)}`);
});
Keith
  • 22,005
  • 2
  • 27
  • 44
  • very nice solution. my 2 cents : it might not be "Promise.all(proms)" at line 10, because you might want the "p.then()" built at line 5 to resolve before stating that Promise.all is done. this can happend if those "then" do call some async like a dom update or so (in which case you might need to some new promise for then to return).. – user3617487 Jul 13 '18 at 13:05
  • @user3617487 Thanks,.. `p.then` is a promise, it should not resolve until it's finished, it should not care if it's calling some `async` code, if it does then this will need to be part of the promise. IOW: `p.then` should not finish, until it's finished.. :) – Keith Jul 13 '18 at 13:18
  • Put aside the point on p.then returning a promise or not. just keep the point that "Promise.all(proms);" won't wait for the updates on progess. but again it's a detail if those update are sync. – user3617487 Jul 13 '18 at 14:19
  • @user3617487 Not really sure what your getting at, `Promise.all` will only resolve when all promises have resolved, that's what `Promise.all` does. You could maybe knock up a snippet showing what you mean, add as an answer here as it's still open. `if those update are sync` updates should never be `sync`,. – Keith Jul 13 '18 at 14:26
  • "return Promise.all(proms); " wait for the promises from "allProgress([test(1000), test(3000), test(2000), test(3500)], ... " these will complete before "then(()=> { d ++; .... " is executed. I fear I cannot type code in comment ?.. I will add a snipet in another answer – user3617487 Jul 13 '18 at 14:50
  • Suggestion: change `forEach` to a `for...of` loop so that `proms` can be any Iterable and not just an array. – Patrick Roberts Nov 22 '18 at 20:22
  • This is great. I think this is going to help me. Do you know if this would work for node too. I was trying to use res.write(`${p}`) in the callback but didn't seem to work. Any thoughts on if that might work? – James Zilch Apr 04 '19 at 16:12
  • @JamesZilch Yes, it should work in node without issues. – Keith Apr 04 '19 at 16:22
  • Would this solution work here. `const results = await allProgress(getPostsArray(postData), (p) => { res.write(`${p}`); }) ` – James Zilch Apr 04 '19 at 16:27
  • Yes, should be fine. When you say not working, any errors. `res.write`, looks like your using express, you might want to look into flushing the output, or putting some `\n\n` in there. – Keith Apr 04 '19 at 20:56
11

You can add a .then() to each promise to count whos finished. something like :

var count = 0;

var p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 5000, 'boo');
}); 
var p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 7000, 'yoo');
}); 
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, 'foo');
}); 

var promiseArray = [
  p1.then(function(val) {
    progress(++count); 
    return val 
  }), 
  p2.then(function(val) {
    progress(++count); 
    return val 
  }), 
  p3.then(function(val) {
    progress(++count); 
    return val 
  })
]

function progress(count) {
  console.log(count / promiseArray.length);
}

Promise.all(promiseArray).then(values => { 
  console.log(values);
});
naortor
  • 2,019
  • 12
  • 26
4

This has a few advantages over Keith's answer:

  • The onprogress() callback is never invoked synchronously. This ensures that the callback can depend on code which is run synchronously after the call to Promise.progress(...).
  • The promise chain propagates errors thrown in progress events to the caller rather than allowing uncaught promise rejections. This ensures that with robust error handling, the caller is able to prevent the application from entering an unknown state or crashing.
  • The callback receives a ProgressEvent instead of a percentage. This eases the difficulty of handling 0 / 0 progress events by avoiding the quotient NaN.
Promise.progress = async function progress (iterable, onprogress) {
  // consume iterable synchronously and convert to array of promises
  const promises = Array.from(iterable).map(this.resolve, this);
  let resolved = 0;

  // helper function for emitting progress events
  const progress = increment => this.resolve(
    onprogress(
      new ProgressEvent('progress', {
        total: promises.length,
        loaded: resolved += increment
      })
    )
  );

  // lift all progress events off the stack
  await this.resolve();
  // emit 0 progress event
  await progress(0);

  // emit a progress event each time a promise resolves
  return this.all(
    promises.map(
      promise => promise.finally(
        () => progress(1)
      ) 
    })
  );
};

Note that ProgressEvent has limited support. If this coverage doesn't meet your requirements, you can easily polyfill this:

class ProgressEvent extends Event {
  constructor (type, { loaded = 0, total = 0, lengthComputable = (total > 0) } = {}) {
    super(type);
    this.lengthComputable = lengthComputable;
    this.loaded = loaded;
    this.total = total;
  }
}
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • 1
    `is never fired synchronously` It's fired at exactly the same time as yours. `The chain is not broken`, where is the promise chain broken?. `input can be any iterable`, That's a good idea, updated. `receives a ProgressEvent` be aware support of the ProgressEvent constructor has limited support as shown on that link you provided. – Keith Mar 09 '19 at 01:17
  • @Keith no it's not fired at the same time as mine. Try `Promise.progress([], () => console.log('callback')); console.log('sync');` as opposed to your `allProgress([], () => console.log('callback')); console.log('sync');`. Your first event is fired synchronously, which is bad because promise-based behavior generally guarantees that the callback is _always_ asynchronous. – Patrick Roberts Mar 09 '19 at 11:43
  • @Keith as for "the chain is not broken", I mean you have a continuation on each of your promises that calls your callback. None of those continuations are being passed to the consumer so there's no way to implement proper error handling if your callback throws. In my code, each of the continuations are what get passed to `Promise.all()` and returned to the consumer, so the chain is not broken. – Patrick Roberts Mar 09 '19 at 11:52
  • @Keith and lastly your argument for the limited support is somewhat irrelevant. I'm happy to add that addendum to my answer and I will, but the whole point is the user gets more information out of the callback value than just a percentage. `0%` and `loaded: 0, total: 0` are definitely not the same thing, for example, so with your code, an empty iterable has no way of indicating to the user that completion was ever reached since you only provide percentage completion. I could just as easily have written my own progress event constructor and used that instead. – Patrick Roberts Mar 09 '19 at 11:56
  • 1
    `guarantees that the callback is always asynchronous.` A callback is not a promise, there is no guarantee that says callbacks have to be async. `proper error handling if your callback throws.` It makes sense here to handle the error in the callback. My answer has not tried to improve on what `Promise.all` does, apart from adding a progress callback, it behaves identical to `Promise.all`. – Keith Mar 11 '19 at 14:28
  • Of course improving `Promise.all` to have better error handling is not a bad thing, and maybe making the callback into promises might be an idea too. Also there appears to be some bugs in your code, tried to use it and get errors. I've a feeling its your `.map(this.resolve)` is the problem as your calling it `promises` and then mapping the resolve, could you try making your answer into a snippet?. – Keith Mar 11 '19 at 14:28
  • @Keith good catch on the bug, I added the fix. As for the asynchronicity, it is a legitimate concern. What if the rest of your set up code for handling progress events comes _synchronously after_ the call to `Promise.progress()`? You'll miss the first callback with your code even though the setup is synchronous. It's about consistent behavior, and it is why all promise-based native methods guarantee that their callbacks occur asynchronously. – Patrick Roberts Mar 11 '19 at 14:35
  • Good catch on the `an empty iterable`, going to add support for that, and also if errors are caught, and I've also noticed that if there is an error, it won't go to 100% either, so I'll update that too. I'm really not sure what problem your pointing out with the callback. You maybe need to show a demo!! – Keith Mar 11 '19 at 14:38
  • @Keith well one easy problem to verify is that if the callback throws an error, the function throws synchronously rather than returning a rejected promise. It's typically bad API design to signal errors in multiple ways. – Patrick Roberts Sep 23 '19 at 14:48
  • Personally I would handle the error in the callback. Otherwise you have now changed the behaviour of `promise.all`, it now has side effects. If errors in the callback is something you do want to propagate then this version is certainly an idea. – Keith Sep 23 '19 at 15:42
  • @Keith I haven't changed the behavior of `Promise.all()`, nor does it have side-effects??? If an error occurs in a callback, your code throws an exception _synchronously_, _OR_ it creates an unhandled promise rejection, depending on which time it's invoked. My solution at least has the courtesy to allow the caller to handle it, and in a consistent manner (rejected promise), if it occurs. – Patrick Roberts Sep 23 '19 at 15:53
  • It does,.. An example is required here. Lets assume all our `promise.all`'s are replaced with this one. Now lets assume all our `promise.all` would have been successful, but a small bug in our callback generated an error. Your code is now going to create a rejected promise. Now this might have been adding records to a database etc,. A side effect here is that it's now rejected, even though the records were successfully saved. Now this might be what your after, and that's fine, but there certainly is a side effect. – Keith Sep 23 '19 at 16:03
  • `progress()` was never intended to be a drop-in replacement for `Promise.all()`. They do not fulfill the same purpose. Your premise that "let's assume our `Promise.all()` are replaced with this one" is moot. – Patrick Roberts Sep 23 '19 at 16:12
  • I'm not sure it's moot?, it's kind of relevant, as I did say in my answer `pass your promises as normal`. PS: I can certainly see were your coming from with your version, and if errors in the callback should be propagated, yours would be the better option here. – Keith Sep 23 '19 at 16:40
0

@Keith in addition to my comment, here is a modification

(edited to fully detail hopefuly)

// original allProgress
//function allProgress(proms, progress_cb) {
//  let d = 0;
//  progress_cb(0);
//  proms.forEach((p) => {
//    p.then(()=> {    
//      d ++;
//      progress_cb( (d * 100) / proms.length );
//   });
//  });
//  return Promise.all(proms);
//}

//modifying allProgress to delay 'p.then' resolution
//function allProgress(proms, progress_cb) {
//     let d = 0;
//     progress_cb(0);
//     proms.forEach((p) => {
//       p.then(()=> {
//         setTimeout( //added line
//           () => {
//                 d ++;
//                 progress_cb( (d * 100) / proms.length );
//           },       //added coma :)
//           4000);   //added line
//       });
//     });
//     return Promise.all(proms
//            ).then(()=>{console.log("Promise.all completed");});
//            //added then to report Promise.all resolution
// }

//modified allProgress
// version 2 not to break any promise chain
function allProgress(proms, progress_cb) {
    let d = 0;
    progress_cb(0);
    proms.forEach((p) => {
      p.then((res)=> {                        //added 'res' for v2
        return new Promise((resolve) => {     //added line for v2
          setTimeout(   //added line
              () => {
                    d ++;
                    progress_cb( (d * 100) / proms.length );
                    resolve(res);             //added line for v2
              },        //added coma :)
            4000);      //added line
        });                                   //added line for v2
      });
    });
    return Promise.all(proms
                   ).then(()=>{console.log("Promise.all completed");});
                   //added then chaining to report Promise.all resolution
}


function test(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
       console.log(`Waited ${ms}`);
       resolve();
     }, ms);
  });
}


allProgress([test(1000), test(3000), test(2000), test(3500)],
  (p) => {
     console.log(`% Done = ${p.toFixed(2)}`);
});

"Promise.all completed" will output before any progress message

here is the output that I get

% Done = 0.00
Waited 1000
Waited 2000
Waited 3000
Waited 3500
Promise.all completed
% Done = 25.00
% Done = 50.00
% Done = 75.00
% Done = 100.00
user3617487
  • 155
  • 11
  • I think you getting yourself confused here, why put a `setTimeout` in the `then` callback.? Your basically breaking the promise chain.. – Keith Jul 13 '18 at 15:02
  • Is that so ? As I understand it, setTimeout is just a common tool to simulate an async calculation (here for instance some updates in the dom). Indeed I don't get what you're refering to as "the promise chain". If you could enlight me :) ? – user3617487 Jul 14 '18 at 16:27
  • `setTimeout` is great for simulating an `async` operation, but on it's own not a `Promise`, that's why in my `test` function it was wrapped inside a Promise constructor. Anyway, the good news the `allProgress` function I created will work identical to `Promise.all`, with the added bonus there is a progress callback, and that's the main. Promise chaining -> https://javascript.info/promise-chaining – Keith Jul 15 '18 at 10:07
0

Here's my take on this. You create a wrapper for the progressCallback and telling how many threads you have. Then, for every thread you create a separate callback from this wrapper with the thread index. Threads each report through their own callback as before, but then their individual progress values are merged and reported through the wrapped callback.

function createMultiThreadProgressWrapper(threads, progressCallback) {
  var threadProgress = Array(threads);

  var sendTotalProgress = function() {
    var total = 0;

    for (var v of threadProgress) {
      total = total + (v || 0);
    }

    progressCallback(total / threads);
  };

  return {
    getCallback: function(thread) {
      var cb = function(progress) {
        threadProgress[thread] = progress;
        sendTotalProgress();
      };

      return cb;
    }
  };
}

// --------------------------------------------------------
// Usage:
// --------------------------------------------------------

function createPromise(progressCallback) {
  return new Promise(function(resolve, reject) {
    // do whatever you need and report progress to progressCallback(float)
  });
}

var wrapper = createMultiThreadProgressWrapper(3, mainCallback);

var promises = [
  createPromise(wrapper.getCallback(0)),
  createPromise(wrapper.getCallback(1)),
  createPromise(wrapper.getCallback(2))
];

Promise.all(promises);
Aleksey Gureiev
  • 1,729
  • 15
  • 17
0

You can use my npm package with an extended version of the native promise, that supports advanced progress capturing, including nested promises, out of the box Live sandbox

import { CPromise } from "c-promise2";

(async () => {
  const results = await CPromise.all([
    CPromise.delay(1000, 1),
    CPromise.delay(2000, 2),
    CPromise.delay(3000, 3),
    CPromise.delay(10000, 4)
  ]).progress((p) => {
    console.warn(`Progress: ${(p * 100).toFixed(1)}%`);
  });

  console.log(results); // [1, 2, 3, 4]
})();

Or with concurrency limitation (Live sandbox):

import { CPromise } from "c-promise2";

(async () => {
  const results = await CPromise.all(
    [
      "filename1.txt",
      "filename2.txt",
      "filename3.txt",
      "filename4.txt",
      "filename5.txt",
      "filename6.txt",
      "filename7.txt"
    ],
    {
      async mapper(filename) {
        console.log(`load and push file [${filename}]`);
        // your async code here to upload a single file
        return CPromise.delay(1000, `operation result for [${filename}]`);
      },
      concurrency: 2
    }
  ).progress((p) => {
    console.warn(`Uploading: ${(p * 100).toFixed(1)}%`);
  });

  console.log(results);
})();
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7
  • Please do mention that you are the author of the lib. – Augustin Riedinger May 09 '21 at 14:32
  • @AugustinRiedinger Hmm. Is there any reason or rule for this? Should I always make this remark if I am currently the only author or co-author/project collaborator, or even a contributor? Where is the line? – Dmitriy Mozgovoy May 09 '21 at 14:58
  • 1
    Your answer is biased as you want to encourage people to use your lib. But on SO, people are looking for the most straightforward, unbiased answer. Besides, using a lib *may* be a solution in some specific cases, but most of the time it is not adapted. Hence this answer could be seen as personal advertising. – Augustin Riedinger May 09 '21 at 15:15
  • 1
    @AugustinRiedinger So if I come up with a solution using NestJS / EventEmitter or EventEmitter2 to solve the issue, would I also be biased as I am a co-author? There are no remarks that the solution is the best and the only one that can exist, this is just one of the possible solutions. Everyone can make their own choice from the proposed. Basically, I don't mind adding this info, but I'm afraid it will look very weird or even boastful. – Dmitriy Mozgovoy May 09 '21 at 15:49
  • 1
    > However, you *must* disclose your affiliation in your answers. – Augustin Riedinger May 10 '21 at 00:29
  • I'm no SO guru, also think admins tend to be often too strict, but this doesn't feel correct here! – Augustin Riedinger May 10 '21 at 00:32
  • @AugustinRiedinger Ok, done, but they seem to be talking about some kind of referring to some own commercial products and/or their promotion sites, not open-source non-profitable projects. Why is the answer with inlined raw source code where you are trying to implement your own untested solution is unbiased, but if you previously published the solution as a library and now referring to it, making your answer biased? Following this logic, it turns out that everyone who publishes the answer believes in their own solution, so it turns out to be a priori biased. – Dmitriy Mozgovoy May 10 '21 at 11:04
  • Cheers. "everyone who publishes the answer believes in their own solution", I make your point philosophically, but in practice 1. on SO, IMHO people expect more *solutions* than *tools* from a lib and 2. I guess the point is to avoid confusion between the *standard* way of doing something and some custom local solution that may not be maintained in the future. – Augustin Riedinger May 10 '21 at 13:14
0
const dataArray = [];
let progress = 0;

Promise.all(dataArray.map(async (data) => {
    await something();

    console.log('progress = ', Math.celi(progress++ * 100 / dataArray.length))
}))
Akhil Ramani
  • 391
  • 4
  • 6