2

Here is a function to build db queries:

const buildDbQueries = async elements => elements.reduce(
  async (acc, element) => {
    // wait for the previous reducer iteration
    const { firstDbQueries, secondDbQueries } = await acc
    const asyncStuff = await someApi(element)

    // leave if the API does not return anything
    if (!asyncStuff) return { firstDbQueries, secondDbQueries }

    // async db query, returns a Promise
    const firstDbQuery = insertSomethingToDb({ 
      id: asyncStuff.id, 
      name: asyncStuff.name
    })

    // another async db query, returns a Promise 
    // have to run after the first one
    const secondDbQuery = insertAnotherthingToDb({
      id: element.id, 
      name: element.name,
      somethingId: asyncStuff.id
    })

    return {
      firstDbQueries: [...firstDbQueries, firstDbQuery],
      secondDbQueries: [...secondDbQueries, secondDbQuery]
    }
  },
  // initial value of the accumulator is a resolved promise
  Promise.resolve({
    firstDbQueries: [],
    secondDbQueries: []
  })
)

This function returns promises which should not be executed until they are resolved.

Now we use that function

const myFunc = async elements => {

  const { firstDbQueries, secondDbQueries } = await buildDbQueries(elements)

  // we don't want any query to run before this point

  await Promise.all(firstDbQueries)
  console.log('Done with the first queries')

  await Promise.all(secondDbQueries)
  console.log('Done with the second queries')
}

The problems are:

  • the queries are executed before we call Promise.all.
  • the firstDbQueries queries are not executed before the secondDbQueries causing errors.

EDIT

As suggested in a comment, I tried not to use reduce, but a for … of loop.

const buildDbQueries = async elements => {
  const firstDbQueries = []
  const secondDbQueries = []

  for (const element of elements) {
    const asyncStuff = await someApi(element)

    // leave if the API does not return anything
    if (!asyncStuff) continue

    // async db query, returns a Promise
    const firstDbQuery = insertSomethingToDb({ 
      id: asyncStuff.id, 
      name: asyncStuff.name
    })

    // another async db query, returns a Promise 
    // have to run after the first one
    const secondDbQuery = insertAnotherthingToDb({
      id: element.id, 
      name: element.name,
      somethingId: asyncStuff.id
    })

    firstDbQueries.push(firstDbQuery)
    secondDbQueries.push(secondDbQuery)

  }

  return { firstDbQueries, secondDbQueries }
}

This still produces the exact same problems as the previous version with reduce.

François Romain
  • 13,617
  • 17
  • 89
  • 123
  • `await acc` and `await buildDbQueries(elements)` will not accomplish anything, because `acc` is neither a thenable object or a `Promise`. – Patrick Roberts Feb 06 '19 at 20:14
  • `Promise.all()` is to run parallel. `await Promise.all([firstDbQueries(), secondDbQueriesl()]);` – zer00ne Feb 06 '19 at 20:41
  • @PatrickRoberts when I log `acc`, it returns `Promise { firstDbQueries: [], secondDbQueries: [] }`. It looks like a promise, no? – François Romain Feb 06 '19 at 21:06
  • @zer00ne I seprate the two `Promise.all` to make explicit that I want to run all the `firstDbQueries` before the `secondDbQueries`. – François Romain Feb 06 '19 at 21:07
  • @FrançoisRomain ah, you're right that the accumulator (except for the first iteration) is a promise, however, awaiting the accumulator does not await the underlying promises in `firstDbQueries` and `secondDbQueries`. The main issue is that you do not await `insertSomethingToDb()` before calling `insertAnotherthingToDb()` – Patrick Roberts Feb 06 '19 at 21:11
  • @PatrickRoberts the `acc` does log a promise from the very first iteration. The goal of the `buildDbQueries` function is to build the queries which are called in `myFunc` in the order we want. They should be called sequentially – François Romain Feb 06 '19 at 21:17
  • 1
    Well, you're wrong. `acc` is `{ firstDbQueries: [], secondDbQueries: [] }` on the first iteration, which is not a promise. Assuming that `insertSomethingToDb()` and `insertAnotherthingToDb()` both return promises, neither of them are being awaited either by `await acc`, or by `await buildDbQueries(elements)`. They _are_ awaited by `await Promise.all(firstDbQueries)` and `await Promise.all(secondDbQueries)` but the problem as I said is that you are not awaiting `insertSomethingToDb()` before calling `insertAnotherthingToDb()` for each element, so they are run in parallel anyway. – Patrick Roberts Feb 06 '19 at 21:23
  • @FrançoisRomain if you want synchronous just `await firstDbQueries()` then next line `await secondDbQueries()` (that is once your other issue is resolved). – zer00ne Feb 06 '19 at 21:34

1 Answers1

3

Don't use an async reducer. Especially not to build an array of promises. Or an array of things to run later. This is wrong on so many levels.

I guess you are looking for something like

function buildDbQueries(elements) {
  return elements.map(element =>
    async () => {
      const asyncStuff = await someApi(element)
      // leave if the api doesn't return anything
      if (!asyncStuff) return;

      await insertSomethingToDb({ 
        id: asyncStuff.id, 
        name: asyncStuff.name
      });
      return () =>
        insertAnotherthingToDb({
          id: element.id, 
          name: element.name,
          somethingId: asyncStuff.id
        })
      ;
    }
  );
}

async function myFunc(elements) {
  const firstQueries = buildDbQueries(elements)

  // we don't want any query to run before this point

  const secondQueries = await Promise.all(firstQueries.map(query => query()));
  //                              this call actually runs the query ^^^^^^^
  console.log('Done with the first queries');

  await Promise.all(secondQueries.map(query => query()));
  //         this call actually runs the query ^^^^^^^
  console.log('Done with the second queries')
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • thank you for your answer. Can you elaborate on what is wrong with the async reducer, please? – François Romain Feb 06 '19 at 21:42
  • You are prone to unhandled rejections if [you don't `await` the accumulator promise immediately](https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation). It just makes no sense conceptually. Use a simple `for … of` loop instead. – Bergi Feb 06 '19 at 21:50
  • I tried with a `for … of ` loop, but still the queries are executed before we call `Promise.all`. Could you please show how to do it? – François Romain Feb 07 '19 at 11:22
  • Given that the code in your question doesn't work, it's really unclear which parts you want to run concurrently and which sequentially. The code in my answer will only create function and not run any queries before the line with `map` and `Promise.all`. – Bergi Feb 07 '19 at 11:49
  • 1
    My question was due to a mis-understanding of how promises work. I was thinking that a promise was not trying to resolve until it was awaited, but it is trying to resolve as soon as it is called, and await just block if it is not resolved at a particular point. Which totally makes sense. Thank you for your answer. – François Romain Feb 08 '19 at 13:41