Skip to the content.

Example: Migrate async.waterfall to ES2018

How to rewrite async.waterfall with async/await?

Remember: Awaiting is meaningful with Promises only.

Here’s some original code

users.authentication.server.controller.js

/**
 * Confirm email POST from email token
 */
exports.confirmEmail = function (req, res) {
  async.waterfall(
    [
      function (done) {
        // Check if user exists with this token
        User.findOne(
          {
            emailToken: req.params.token,
          },
          function (err, user) {
            if (!err && user) {
              // Will be the returned object when no errors
              var result = {};

              // If users profile was hidden, it means it was first confirmation email after registration.
              result.profileMadePublic = !user.public;

              done(null, result, user);
            } else {
              return res.status(400).send({
                message: 'Email confirm token is invalid or has expired.',
              });
            }
          },
        );
      },

      // Update user
      // We can't do regular `user.save()` here because we've got user document with password and we'd just override it:
      // Instead we'll do normal Mongoose update with previously fetched user ID
      function (result, user, done) {
        User.findOneAndUpdate(
          { _id: user._id },
          {
            $unset: {
              emailTemporary: 1,
              emailToken: 1,
              // Note that `publicReminderCount` and `publicReminderSent` get reset now each
              // time user confirms any email change, even if they didn't confirm their profile yet.
              // That's fine: we'll just start sending 'finish signup' notifications from scratch
              // to the new email. That old email before the change might've been wrong anyway...
              publicReminderCount: 1,
              publicReminderSent: 1,
            },
            $set: {
              public: true,
              // Welcome sequence emails are sent in time intervals
              welcomeSequenceSent: new Date(),
              // Replace old email with new one
              email: user.emailTemporary,
              // @todo: this should be done at user.server.model.js
              emailHash: crypto
                .createHash('md5')
                .update(user.emailTemporary.trim().toLowerCase())
                .digest('hex'),
            },
          },
          {
            // Return the document after updates if `new = true`
            new: true,
          },
          function (err, modifiedUser) {
            done(err, result, modifiedUser);
          },
        );
      },

      function (result, user, done) {
        req.login(user, function (err) {
          done(err, result, user);
        });
      },

      function (result, user) {
        // Return authenticated user
        // Remove sensitive data befor sending user
        result.user = userProfile.sanitizeProfile(user);

        return res.json(result);
      },
    ],
    function (err) {
      if (err) {
        return res.status(400).send({
          message: errorService.getErrorMessage(err),
        });
      }
    },
  );
};

…and here is the same code written with async/await

/**
 * Confirm email POST from email token
 */
exports.confirmEmail = async function (req, res) {
  // Check if user exists with this token
  // ***** We await Promise instead of using callback ***** //
  let user;
  try {
    user = await User.findOne({ emailToken: req.params.token }).exec(); // ***** this returns Promise ***** //
    if (!user) throw new Error('user not found'); // ***** the logic changed a bit ***** //
  } catch (err) {
    // ***** we catch mongoose errors and also 'user not found' error from above ***** //
    return res.status(400).send({
      message: 'Email confirm token is invalid or has expired.',
    });
  }

  try {
    // ***** This is a big try/catch block that takes care of all unexpected errors ***** //
    // Will be the returned object when no errors
    var result = {};

    // If users profile was hidden, it means it was first confirmation email after registration.
    result.profileMadePublic = !user.public;

    // Update user
    // We can't do regular `user.save()` here because we've got user document with password and we'd just override it:
    // Instead we'll do normal Mongoose update with previously fetched user ID
    const modifiedUser = await User.findOneAndUpdate(
      // ***** returns a Promise ***** //
      { _id: user._id },
      {
        /*****
          ... no need to repeat ...
        *****/
      },
      {
        // Return the document after updates if `new = true`
        new: true,
      },
    ).exec(); // again, this is how we make a mongoose query return a Promise

    const util = require('util'); // we normally keep `require` on top of the file
    const promisifiedLogin = util.promisify(req.login); // make a Promise function from callback function

    await promisifiedLogin(modifiedUser); // ***** hey we didn't test this, but it probably works ***** //

    // Return authenticated user
    // Remove sensitive data befor sending user
    result.user = userProfile.sanitizeProfile(modifiedUser);

    return res.json(result);
  } catch (err) {
    // ***** here we catch unexpected errors and return an error message ***** //
    return res.status(400).send({
      message: errorService.getErrorMessage(err),
    });
  }
};