Start using async/await instead of promises and callbacks

sorry @robfallows but it doesn’t. Async/Await stopped working a few months ago (this summer?). Now I have to use Promise.await().

It works in NPM modules, but as soon as the async is somewhere near client side code, it stops working.

I opened a few topics and filed a github issue, but I got no response.

Do you have a repro? I’ve been using async/await on the client and server without issue (other than in-server method calls as mentioned). I’m currently on 1.4.2.

The important thing to remember is that if you “promisify” a Meteor method, the Promise is resolved before the data is sent over the wire to the client. That means that you can, if you wish, continue to use the standard, callback form of Meteor.call and it will work as it always did.

However, you can “promisify” the call. I use something like this for a promised call:

const callWithPromise = (method, myParameters) => {
  return new Promise((resolve, reject) => {
    Meteor.call(method, myParameters, (err, res) => {
      if (err) reject('Something went wrong');
      resolve(res);
    });
  });
}

and then inside an async function:

const result = await callwithPromise('someMethod', { some: parameter, someOther: parameter });
7 Likes

Hmm, interestingly what I did which works is I just returned the promise in the method and then in the Meteor call all I did was await, seems a lot simpler than what you’re doing here and works for me but maybe there’s a reason why I shouldn’t do this?

I just trial and errored here, maybe this is not the right approach :smiley:

@robfallows using your example just above, can you show an example of “chaining” promises too?

Chaining async calls using promises to me is the real value here.

Hmm. Do you have a repo with this approach? I’ve just tried what (I think) you said and it doesn’t work in the way you explained.

  • The Promise object is not returned from the method. Instead, the Promise is resolved in the method and the result is returned. This makes sense, because if you think about it, a Promise may, for example, be waiting on an external REST endpoint. The external response will not be sent to your client for resolution.
  • If I do const result = await Meteor.call('someMethod'); I always get undefined: the method is called (so something happens on the server), but there is no Promise to await, so the call completes without waiting.

Using await makes chaining Promises almost as simple as using sync-style coding on the server. In principle I would take something like that callWithPromise and just use multiple calls:

const thing = await callWithPromise('methodA'), {} );
const factor = await callWithPromise('methodB', { item: thing } );
const result = await callWithPromise('methodC', { item: thing, factor } );

The only “gotchas” you need to be aware of are:

  1. You can only await inside an async function. However, you can do funky things like Template.bob.onCreated(async function() {... which makes your entire onCreated async.
  2. There is still likely to be a resolution step if you want to get a result from an await chain out of your async function and back into your “normal” code. In Meteor you can do this easily by making use of ReactiveVars.

So, in Blaze, you could do something like:

Template.bob.onCreated(async function() {
  this.result = new ReactiveVar();
  const thing = await callWithPromise('methodA'), {} );
  const factor = await callWithPromise('methodB', { item: thing } );
  this.result.set(await callWithPromise('methodC', { item: thing, factor } ));
}

Template.bob.helpers({
  getResult() {
    return Template.instance().result.get();
  }
});
5 Likes

Thanks rob. But you can’t use the .then syntax somehow?

let calculate = function (value) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(value + 1);
        }, 0);
    });
}; 

calculate(1)
    .then(calculate)
    .then(result => result + 1)
    .then(calculate)
    .then(verify);
 
function verify(result) {
    expect(result).toBe(5);
    done();
};

Yes. They’re standard Promises (at least insofar as any of the plethora of Promises are standard).

I tried this but I’m not getting the result from the client via console.log, the server is fine though.

export const useProfileAddress = new ValidatedMethod({
  name: 'cart.useProfileAddress',
  mixins: [CallPromiseMixin],
  validate: new SimpleSchema({
  }).validator(),
  run(args) {
    return (async function() {
      try {
        const storeId = await getClosestStore.callPromise()
        console.info('storeId', storeId) // This part returns something on the Server but it's undefined on the client.
      } catch (e) {
        console.log(e.message);
      }
    }())
  }
})

export const getClosestStore = new ValidatedMethod({
  name: 'store.getClosest',
  mixins: [CallPromiseMixin],
  validate: new SimpleSchema({
  }).validator(),
  run(args) {
    const store = Store.findOne() // Could it be this?
    return store._id
  }
})

Am I missing something? Or should I enclose all my method in Meteor.isServer?

Try this:

export const useProfileAddress = new ValidatedMethod({
  name: 'cart.useProfileAddress',
  mixins: [CallPromiseMixin],
  validate: new SimpleSchema({
  }).validator(),
  async run(args) {
    try {
      const storeId = await getClosestStore.callPromise();
      console.info('storeId', storeId); // This part returns something on the Server but it's undefined on the client.
      return storeId;
    } catch (e) {
      console.log(e.message);
    }
  },
});
5 Likes

Thank you @robfallows that lessened my lines of code but I’m still not getting the storeId from the browser console. So I’ve enclosed my code in Meteor.isServer to inhibit the error. Could it be because the method is trying to get it from minimongo and not from the Mongo since it was simulated on the client?

Particularly this line on my getClosestStore method?

const store = Store.findOne()
    return store._id

That’s certainly possible: do you have a subscription to your Store collection which should make this available on the client?

The other piece I’m confused about is getClosestStore - is this a method for a reason? It looks like it could be completely replaced with Store.findOne()._id;. (I also can’t see why that will return the closest store, but maybe you’ve just simplified that for the post).

@robfallows yes there is suppose to be a logic before sending in the store, so I’ve just simplified the code to make it easy.

That I don’t have, I am new to Meteor and I thought the method will return that for me like REST would do. So should I do a subscription in the client then huh? But everything works by enclosing the method inside Meteor.isServer though, but I’m just not sure if it’s a good practice or what.

No. A method is used for “give me this data right now”: https://guide.meteor.com/methods.html

If you’re using Collection.find() you need to subscribe (on the client) to a publish method on the server. That pub/sub gives you a “live” connection on the client to the database on the server - or a small subset of the database: https://guide.meteor.com/data-loading.html

2 Likes

Hi guys,

Interesting read about the async and promises. I tried to use callPromise from deanius but with a modern Typescript project that is not going to work since you can’t amend the Meteor object typing to add the definition for callPromise.

I tried to use await but I am not sure if this is going to work since my target is es5 (needs to run on IE 10+) and I am not sure if await is going to be polyfilled.

In the end I just used the promise wrapper

export const callWithPromise = ( method, myParameters ) => {
  return new Promise( ( resolve, reject ) => {
    Meteor.call( method, myParameters, ( err, res ) => {
      if ( err ) reject( err );
      resolve( res );
    } );
  } );
}

so that at least I can use the .then function.

Does anyone have a nice example of this await setup with an async method call using typescript?

thanks @robfallows for your many interventions. I have read a couple of your posts on async/await and though it still seems a bit above my head, I have some spaghetti code that sometime runs into race conditions and I believe this async/await will help resolve it.
this answer right here explained how I can setup my server methods ( I am thinking only converting the methods called on the client and involved in this call back from h**)

so on the client I have the following: how do I rewrite it to take advantage of async/await, to be more predictable and cleaner after converting the server side methods as you have described here?

AutoForm.hooks({
  addOrganizationForm:{
      onSubmit: function (insertDoc, updateDoc, currentDoc) {
        this.event.preventDefault();
        var self = this;
        // TODO: create a record of this in orders before continuing, NB approve instantly
        // 1. setup orders object
        // 2. create order and on sucess
        // 3. create organization
        let insertOrdersArgs = {
          orderType: 'registerOrganization',
          orderDetails: insertDoc,
          orderedByEntityType: Meteor.user().currentType ? Meteor.user().currentType : Meteor.user().initialType,   // for first run there is no currentType 
          orderStatus: "Approved", // for now, regeistering a organization should be aproved immediately, may change in future
          pending: false,
          approved: true,
          approvedAt: new Date(),
          // approvedBy: Meteor.userId() not necessary, handled in db         
        };
        Meteor.call('orders.insert', insertOrdersArgs, function(error, result) {           
          if (error) {
            // if error regestirng the order 
            Bert.alert({
              title: error.error,
              message: error.reason,
              type: 'danger',
              style: 'growl-top-right',
              icon: 'fa-frown-o',
              hideDelay: 5000,
              });
            return false;
            
          } 
          // else {
          if(result){ // if order is created,
            // actually create the organization now
            Meteor.call('organizations.insert', insertDoc, function(error, result) {              
              if (error) {
                Bert.alert({
                    title: error.error,
                    message: error.reason,
                    type: 'danger',
                    style: 'growl-top-right',
                    icon: 'fa-frown-o',
                    hideDelay: 5000,
                });
              } 
              if(result){ // if organization is created,
                // assign Creator role of this lincesor (group) to this user
                // Roles.addUsersToRoles(joesUserId, ['manage-team','schedule-game'], 'manchester-united.com')
                // Roles.addUsersToRoles(Meteor.userId(), ['Creator','Assignee'], licensorId);
                let updateRoleArgs = {
                    group: result, // saving organization._id as group name
                    roles: ['Creator'] //   roles: ['Creator','Assignee']
                  };
        
                // update user, 
                Meteor.call('users.assignUserRoles', updateRoleArgs , function(error, result){       
                    if(error){
                        Bert.alert({
                          title: error.error,
                          message: error.reason,
                          type: 'danger',
                          style: 'growl-top-right',
                          icon: 'fa-frown-o',
                          hideDelay: 5000,
                      });
                    } 
                    else {
                          // if(result){ // if role is assigned, doenst retunr a valid result
                          // update user's currentType, associatedOrganizations and currentOrganization, 
                          // 1. update user's currentType
                          // 2. update user's associatedOrganizations with new associatedOrganization (organization) id
                          // 3. set user's currentOrganization
                          // which will reactively update the reactiveVars currentUserType, currentOrganization
                          let userUpdateArgs = {
                            _id: Meteor.userId(),
                            modifier: {
                              // for cases where currentType not set to assignee, eg user later decided to setup a organization, long after first run stuff
                              $set:{currentType: "Creator", currentOrganization: updateRoleArgs.group}, 
                              $push: {'associatedOrganizations': updateRoleArgs.group} // for when using a simple array to store ids
                              }
                          };
              
                          // update, 
                          Meteor.call('users.update', userUpdateArgs , function(error, result){
                              if(error){
                                    Bert.alert({
                                    title: error.error,
                                    message: error.reason,
                                    type: 'danger',
                                    style: 'growl-top-right',
                                    icon: 'fa-frown-o',
                                    hideDelay: 5000,
                                });
                              } 
                             if(result){
                                  Meteor.call('subscriptions.insert', {
                                  subscriptionType: "Follower",
                                  subscriber: Meteor.userId(),
                                  subscriberType: "User",
                                  subscribedTo: Meteor.user().currentOrganization,
                                  subscribedToType: "Organisation"  
                                } , function(error, result) {   
                                if(error){
                                    console.log(error)        
                                }
                                console.log(result);
                              });
                                  
                                  // Modal.hide();
                                  FlowRouter.go('/dashboard');
                              }
                          });
                      
                      }
                });                  

                self.done();
              }
            });           
          }
        });     
        return false;
      },

You could use my async package! https://atmospherejs.com/npdev/async-proxy

There is a note in there about legacy browsers, which is out of date. Just use my wrapper package for that, and get a good enough proxy polyfill in your legacy bundle, with no overhead in your main bundle: https://atmospherejs.com/npdev/legacy-proxy-polyfill

3 Likes

What you do in principle for each Meteor.call is:

  1. Ensure the closest enclosing function is async. That looks like onSubmit: function (insertDoc, updateDoc, currentDoc) { becomes async onSubmit(insertDoc, updateDoc, currentDoc) { in your example.

  2. Assign the appropriate result variable to await each call and use try/catch for the error, so:

    Meteor.call('abc', p1, p2, (error, result) => {
      //callback code
    });
    

    becomes:

    try {
      const result = await callWithPromise('abc', p1, p2);
    } catch (error) {
      // handle error
    }
    
  3. Flatten the nesting.

    const result1 = await callWithPromise('abc', p1, p2);
    const result2 = await callWithPromise('def', result1);
    const result3 = await callWithPromise('def', result2, p3);
    // ...
    

Using the npdev:async-proxy package means callWithPromise('abc', ...) becomes callAsync('abc', ...) or Methods.abc(...). Similarly, if you were to use the deanius:promise package, you’d use Meteor.callPromise('abc',...).

Note, there is a gotcha when using const (and let, if you’re not careful), which makes it difficult to try/catch each call separately. Your code suggests you should use something like:

let result1;
try {
  result1 = await callWithPromise('abc', p1, p2);
} catch (error) {
  // ...
}
let result2;
try {
  result2 = await callWithPromise('def', result1);
} catch (error) {
  // ...
}
let result3;
try {
  result3 = await callWithPromise('def', result2, p3);
} catch (error) {
  // ...
}

Good luck!

1 Like

This is what my company did:

export class VivaMethod extends ValidatedMethod {
  asyncCall(args) {
    return new Promise((resolve, reject) => {
      this.connection.apply(this.name, [args], this.applyOptions, (err, res) => {
        if (err) {
          if (Meteor.isClient && !Meteor.isProduction) console.error({ ...err, details: `Error calling '${this.name}'` });
          reject(err);
        }
        resolve(res);
      });
    });
  }
}
1 Like