'Asyncronicity in a reduce() function WITHOUT using async/await

I am patching the exec() function to allow subpopulating in Mongoose, which is why I am not able to use async/await here -- my function will be chained off a db call, so there is no opportunity to call await on it, and within the submodule itself, there I can't add async/await outside of an async function itself.

With that out of the way, let's look at what I'm trying to do. I have two separate arrays (matchingMealPlanFoods and matchingMealPlanRecipeFoods) full of IDs that I need to populate. Both of them reside on the same array, foods. They each require a db call with aggregation, and the problem in my current scenario is that only one of the arrays populates because they are happening asynchronously.

What I am trying to do now is use the reduce function to return the updated foods array to the next run of reduce so that when the final result is returned, I can replace the entire foods array once on my doc. The problem of course is that my aggregate/exec has not yet returned a value by the time the reduce function goes into its next run. Is there a way I can achieve this without async/await here? I'm including the high-level structure here so you can see what needs to happen, and why using .then() is probably not viable.

EDIT: Updating code with async suggestion

function execute(model, docs, options, lean, cb) {
  options = formatOptions(options);
  let resolvedCount = 0;
  let error = false;

  (async () => {
    for (let doc of docs) {
      let newFoodsArray = [...doc.foods];
      for (let option of options) {
        const path = option.path.split(".");
        // ... various things happen here to prep the data
        const aggregationOptions = [
          // // $match, then $unwind, then $replaceRoot
        ];

        await rootRefModel
          .aggregate(aggregationOptions)
          .exec((err, refSubDocuments) => {
            // more stuff happens
            console.log('newFoodsArray', newFoodsArray); // this is to check whether the second iteration is using the updated newFoods Array
            const arrToReturn = newFoodsArray.map((food) => {
              const newMatchingArray = food[nests[1]].map((matchingFood) => {
                //more stuff
                return matchingFood;
              });

              const updatedFood = food;
              updatedFood[`${nests[1]}`] = newMatchingArray;
              return updatedFood;
            });
            console.log('arrToReturn', arrToReturn);
            newFoodsArray = [...arrToReturn];
          });
      }
    };
    console.log('finalNewFoods', newFoodsArray); // this should log after the other two, but it is logging first.
    const document = doc.toObject();
    document.foods = newFoodsArray;

    if (resolvedCount === options.length) cb(null, [document]);
  }
})()

EDIT: Since it seems it will help, here is the what is calling the execute function I have excerpted above.

 /**
   * This will populate sub refs
   * @param {import('mongoose').ModelPopulateOptions[]|
   * import('mongoose').ModelPopulateOptions|String[]|String} options
   * @returns {Promise}
   */
  schema.methods.subPopulate = function (options = null) {
    const model = this.constructor;
    if (options) {
      return new Promise((resolve, reject) => execute(model, [this], options, false, (err, docs) => {
        if (err) return reject(err);
        return resolve(docs[0]);
      }));
    }
    Promise.resolve();
  };
};


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source