Future vs. Promise vs. wrapAsync vs. Async/Await ...... AHHHHH!

Hello :slight_smile:

I hope someone can help me with this not necessarily Meteor related question. I’ve been having a difficult time keeping up with all the rapid changes within the JS space; in particular when it comes to the asynchronous patterns. There’s Meteor.wrapAsync, Future, Promise and Async/Await… I honestly don’t even know where to begin, so I’ll start with an example…

 createProMember (customer) {
    if (Meteor.isServer) {
      check(customer, {
        plan: String,
        token: String
      })

      // Create a Future that we can use to confirm successful account creation.
      const newCustomer = new Future()
      const user = Meteor.user()
      const userEmail = getEmail(user._id)
      const isCustomer = user.profile.pro

      if (!user) {
        throw new Meteor.Error('500', 'You must be a registered user.')
      } else if (isCustomer) {
        throw new Meteor.Error('500', "You're already a customer.")
      }

      // Create our customer.
      Meteor.call('stripeCreateCustomer', customer.token, userEmail, (error, stripeCustomer) => {
        if (error) {
          console.log(error)
        } else {
          const stripeId = stripeCustomer.id
          const plan = customer.plan
          // Setup a subscription for our new customer.
          Meteor.call('stripeCreateSubscription', stripeId, plan, (error, response) => {
            if (error) {
              console.log(error)
            } else {
              try {
                // Perform an update on our new user.
                Meteor.users.update(user, {
                  $set: {
                    'profile.pro': {
                      stripeId: stripeId,
                      subscription: {
                        plan: {
                          name: customer.plan,
                          used: 0
                        },
                        payment: {
                          card: {
                            type: stripeCustomer.sources.data[0].brand,
                            lastFour: stripeCustomer.sources.data[0].last4
                          },
                          nextPaymentDue: response.current_period_end
                        },
                        status: response.status,
                        ends: response.current_period_end
                      }
                    }
                  }
                }, (error, response) => {
                  if (error) {
                    console.log(error)
                  } else {
                    // Once the subscription data has been added, return to our Future.
                    newCustomer.return(user)
                  }
                })
              } catch (exception) {
                newCustomer.return(exception)
              }
            }
          })
        }
      })
      // Return our newCustomer Future.
      return newCustomer.wait()
    }
  }

How should this be coded so as to be inline with the Meteor/JS best practices of 2019.

This is sort of a Meteor related question. But the important thing is that you can write things much simpler. This is assuming you have methods called stripeCreateCustomer and stripeCreateSubscription:

function createProMember(customer) {
    check(customer, {
        plan: String,
        token: String
    });

    const user = Meteor.user();
    const userEmail = getEmail(user._id);
    // TODO: Make sure to remove the insecure package, because profile is
    // writeable by the client by default in that configuration
    const isCustomer = user.profile.pro;

    if (!user) {
        throw new Meteor.Error('500', 'You must be a registered user.')
    } else if (isCustomer) {
        throw new Meteor.Error('500', "You're already a customer.")
    }

    // Create our customer.
    let stripeCustomer = Meteor.call('stripeCreateCustomer', customer.token, userEmail);

    const stripeId = stripeCustomer.id;
    const plan = customer.plan;
    // Setup a subscription for our new customer.
    let response = Meteor.call('stripeCreateSubscription', stripeId, plan);
    // TODO: You probably want to check the response for stuff
    // Perform an update on our new user.
    // TODO: You were updating the user document instead of the _id here
    Meteor.users.update(user._id, {
        $set: {
            'profile.pro': {
                stripeId: stripeId,
                subscription: {
                    plan: {
                        name: customer.plan,
                        used: 0
                    },
                    payment: {
                        card: {
                            type: stripeCustomer.sources.data[0].brand,
                            lastFour: stripeCustomer.sources.data[0].last4
                        },
                        nextPaymentDue: response.current_period_end
                    },
                    status: response.status,
                    ends: response.current_period_end
                }
            }
        }
    });
    return stripeCustomer;
}
  

Meteor.wrapAsync is used to turn a library method that ends with a callback parameter into one that does not:

function typicalLibraryFunction(a, b, callback) {
  // Does a bunch of work, and then eventually...
  callback(error, response);
}

// Normally you would do:
typicalLibraryFunction(a, b, (err, res) = > {...});

// In method bodies, you can instead do
let wrappedTypicalLibraryFunction = Meteor.wrapAsync(typicalLibraryFunction);
// Observe there is no callback passed. Meteor will throw the exception, so you can use this in try-catch.
// This is very similar to how go, Java and other programming languages work
let res = wrappedTypicalLibraryFunction(a, b);

It’s called wrapping because Meteor passes your original function a special callback that makes it easier to write serial code like that.

In method bodies, you should use that Meteor.wrapAsync pattern, and not async / await in practice. You will almost never have to write server code that is executed outside of method bodies. In the event that you do, like for libraries that provide “webhooks,” look at Meteor.bindEnvironment.

There’s a separate question about “rapid changes within the JS space,” you’re asking what is the right “asynchronous” pattern. I’d suggest the pattern above (the one Meteor shipped with).

You can see this matter get litigated a lot on these forums and elsewhere. So beware :slight_smile:

3 Likes

I disagree! (Beware, litigators inbound! :dragon_face:)
Not really, just adding on :laughing:

If you are using a package that returns callbacks, Meteor.wrapAsync is the nicest way to go.

If you’re using a package that return promises, then async/await is much easier:

Meteor.methods({
    createProMember: async function (customer) {
        check(customer, customerSchema);
        const result = await somePromiseBasedApiCall(customer)
        // Meteor collection methods are always sync-like on the server
        // because they run in a fiber so there's no need for a callback
        Meteor.users.update(user, {
              $set: {
                    'profile.pro': { 
                        // etc...
                    },
              },
       });
       return whateverYouWant;
    }
});

Both are nice ways of removing the need for callbacks and makes returning the result of an async call much easier

Yes, @coagmano is right, you really get to do it both ways, so it’s pretty convenient!