Delay between promises when using Promise.all

Viewed 11867

Is there a way to delay the evaluation of an array of promises using Promise.all()?

Does it make sense to manually add a delay function to the end of each promise before adding them to the array?

Promise.all([p1,p2,p3]).then(res => console.log(res))

I would like to add a delay because my server can't handle too many requests at once.

6 Answers

Promise.all is intended to resolve when the promises are fulfilled, but existing promises are evaluated regardless of Promise.all.

In order to do this, promises should be initially created to produce a delay:

const delayIncrement = 500;
let delay = 0;

const p1 = new Promise(resolve => setTimeout(resolve, delay)).then(() => fetch(...));

delay += delayIncrement;

const p2 = new Promise(resolve => setTimeout(resolve, delay)).then(() => fetch(...));

delay += delayIncrement;

...
Promise.all([p1,p2,p3]).then(...);

The same solution can be used for creating request promises in batch inside a loop.

The recipes for delayed promises can be found in this answer.

Yes, you can delay promises using Promise.all to create staggered execution and it's quite easy to do:

// Promise.all() with delays for each promise
let tasks = [];
for (let i = 0; i < 10; i++) {
  const delay = 500 * i;
  tasks.push(new Promise(async function(resolve) {
    // the timer/delay
    await new Promise(res => setTimeout(res, delay));

    // the promise you want delayed
    // (for example):
    // let result = await axios.get(...);
    let result = await new Promise(r => {
      console.log("I'm the delayed promise...maybe an API call!");
      r(delay); //result is delay ms for demo purposes
    });

    //resolve outer/original promise with result
    resolve(result);
  }));
}

let results = Promise.all(tasks).then(results => {
  console.log('results: ' + results);
});

You can run it here too.

Rather than a delay between the chain, which can be done with .then() as shown in other answers, this is a delay that differs for each Promise so that when you call Promise.all() they will be staggered. This is useful when, say, you are calling an API with a rate limit that you'd breach by firing all the calls in parallel.

Peace

I needed to create the calls dynamically, so based on the answer from @estus-flask, managed to come up with:

  let delay = 0; const delayIncrement = 1000;

  const promises = items.map(item => {
    delay += delayIncrement;
    return new Promise(resolve => setTimeout(resolve, delay)).then(() =>
        fetch(...);
  })

  let results = await Promise.all(promises);

The simplest solution for me seemed to be to just take the current index of the map function that produces the promises, and use that index to base a delay on:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

await Promise.all(
  dataPoints.map(async (dataPoint, index) => {
    await sleep(index * 1000)
    ...

This makes each of the operations wait index * 1 second to be fired, effectively placing a 1s delay between each operation.

Is there a way to delay the evaluation of an array of promises using Promise.all()?

No. Promises are not "evaluated", they just resolve. When this happens is determined by their creator and nothing else. When Promise.all is called, the promises p1, p2 and p3 have already been created (and their asynchronous tasks probably already have been started).

Another way you can do this is by hijacking the way loops are transpiled:

async function doABatchOfAsyncWork(workItems) {
  for (const item of workItems) {
    await workTask(item)
    await delay(1000) // not built-in but easily implemented with setTimeout + promise
  }
}

You can also save the values of course and return them at the end, exactly as you usually can in for-loops. You can't do this with map since the await would have to be in the async context of the map-functor passed in. If you used map it would execute everything at ~the same time, with a delay of 1s at the end.

Related