Search code examples
javascriptnode.jsasynchronouspromisees6-promise

Understand Asynchronous and Promise in NodeJS


I have been coding in NodeJS from past 6 months but still, i don't have a clear picture about asynchronous and promise concept. Now coming to the question i will fetch a record using Mongoose from MongoDB which may have branchIds am performing a simple for loop in each iteration am performing a MongoDB operation which is asynchronous(as MongoDB/Mongoose operations are promises). As you know the for loop are synchronous but my function returns the value before the for loop ends. How does it happen? Am attaching the code if my question is not clear leave it as a comment.

const restManageChef = (params, query, body) => {
    if (query && parseBoolean(query.superChef)) {
        body = Object.assign(body, { role: 'SUPER-CHEF' });
    } else {
        body = Object.assign(body, { role: 'RES-CHEF' });
    }

    return restPUT(params, query, body).then(chef => {
        return userModel
        .findOne({ restaurantCode: chef.restaurantCode, type: 'RES-ADMIN' })
        .then(resAdminDetails => {
            log.debug({ Chef: chef }, 'Chef Details');
            if (chef.role === 'SUPER-CHEF') {
            log.debug({ BranchIds: resAdminDetails.branchIds }, 'BranchIds');
            for (let i = 0; i < resAdminDetails.branchIds.length; i) {
                log.debug({ BranchIds: resAdminDetails.branchIds[i] }, 'BranchIds');
                pushChefId(resAdminDetails.branchIds[i], chef.pkid)
                .then(restaurant => {
                    log.debug({ Restaurant: restaurant }, 'Restaurant Details');
                })
                .catch(err => {
                    log.error({ err });
                    throw err;
                });
            }
            return chef;
            } else if (chef.role === 'RES-CHEF') {
            for (let i = 0; i < resAdminDetails.branchIds.length; i++) {
                log.debug({ BranchIds: resAdminDetails.branchIds[i] }, 'BranchIds');
                pushChefId(resAdminDetails.branchIds[i], chef.pkid)
                .then(restaurant => {
                    log.debug({ Restaurant: restaurant }, 'Restaurant Details');
                })
                .catch(err => {
                    log.error({ err });
                    throw err;
                });
            }
            return chef;
            }
        });
    });
};

PushChefId Function

const pushChefId = (restaurantCode, chefId) => {
  return userModel
    .findOneAndUpdate({ restaurantCode }, { $addToSet: { chefIds: chefId } })
    .exec()
    .then(resAdmin => {
      if (!resAdmin) return Promise.reject(`No RES-ADMIN found with restaurantCode - ${restaurantCode}`);

      return storeModel.findByIdAndUpdate(restaurantCode, { $addToSet: { chefIds: chefId } }, { new: true });
    });
};

Solution

  • You are working with asynchronous (Promises in your case) code in a synchronous way.

    This is an async call:

      pushChefId(resAdminDetails.branchIds[i], chef.pkid)
        .then(restaurant => {
          log.debug({
            Restaurant: restaurant
          }, 'Restaurant Details');
        })
        .catch(err => {
          log.error({
            err
          });
          throw err;
        });
    

    Basically you fire this async calls one by one and immediately step over to the return statement, without ever waiting for completion of each fired async call.

    The one approach I would definitely advise in your case to look at is async/await, which is basically a synchronous way of writing asynchronous code.

    It could go something like this:

    const decorateWithRole = (query, body) => {
      return {
        ...body,
        role: (query && parseBoolean(query.superChef) && "RES-CHEF") || "SUPER-CHEF"
      };
    };
    
    const restManageChef = async(params, query, body) => {
      const decoratedBody = decorateWithRole(query, body, parseBoolean);
    
      const chef = await restPUT(params, query, body);
      const resAdminDetails = await userModel.findOne({
        restaurantCode: chef.restaurantCode,
        type: "RES-ADMIN"
      });
    
      log.debug({
        Chef: chef
      }, "Chef Details");
    
      if (["SUPER-CHEF", "RES-CHEF"].includes(chef.role)) {
        log.debug({
          BranchIds: resAdminDetails.branchIds
        }, "BranchIds");
        for (let i = 0; i < resAdminDetails.branchIds.length; i++) {
          log.debug({
            BranchIds: resAdminDetails.branchIds[i]
          }, "BranchIds");
          try {
            const restaurant = await pushChefId(
              resAdminDetails.branchIds[i],
              chef.pkid
            );
            log.debug({
              Restaurant: restaurant
            }, "Restaurant Details");
          } catch (err) {
            log.error({
              err
            });
            throw err;
          }
        }
        return chef;
      }
    };
    
    const pushChefId = async(restaurantCode, chefId) => {
      const resAdmin = await userModel
        .findOneAndUpdate({
          restaurantCode
        }, {
          $addToSet: {
            chefIds: chefId
          }
        })
        .exec();
    
      if (!resAdmin) {
        return Promise.reject(
          `No RES-ADMIN found with restaurantCode - ${restaurantCode}`
        );
      }
    
      return storeModel.findByIdAndUpdate(
        restaurantCode, {
          $addToSet: {
            chefIds: chefId
          }
        }, {
          new: true
        }
      );
    };
    

    Of course it can be optimised with parallel promise triggering etc etc. But for basic explanation should be enough.

    The crucial change is here:

    for (let i = 0; i < resAdminDetails.branchIds.length; i++) {
          log.debug({
            BranchIds: resAdminDetails.branchIds[i]
          }, "BranchIds");
          try {
            const restaurant = await pushChefId(
              resAdminDetails.branchIds[i],
              chef.pkid
            );
            log.debug({
              Restaurant: restaurant
            }, "Restaurant Details");
          } catch (err) {
            log.error({
              err
            });
            throw err;
          }
        }
        return chef;
      }
    };
    

    The await keyword within the context of an async function will await for the resolution of a Promise value and will return it without the Promise wrapper, just the raw value or will receive the thrown error, thus allowing to catch it also in a synchronous way with basic try catch.

    You can read more on async await here.

    Hope this clarifies a bit.