Start using async/await instead of promises and callbacks

Yes - this sort of thing is supported:

Meteor.methods({
  async someMethod(x, y) {
    const a = await someAsyncFunction(x);
    return await someOtherAsyncFunction(a, y);
  }
});

@robfallows have you actually done that? Based on what @ilacyero reports:

ā€¦ I canā€™t use await inside a meteor method, because I should define the method as async and it generates error ā€¦

1 Like

Yes. It works exactly as it should when called from the client (the Promise is resolved before the result is sent back over the wire).

When I was playing with this a few months back I got unexpected results (the Promise itself) when calling an async method from within the same server code (not something I normally do).

Hm, so given a methods-calling-methods situation, eventually some of those methods will be getting called form the server as well. And then it might (based on what I understand from your experience) cause issues, right?

Perhaps. I found that behaviour, but I havenā€™t revisited those tests for quite a while, so it may be fixed now. I should probably take another look. I was putting a repro together for @benjamn, but got sidetracked and didnā€™t finish it. :disappointed:

Hm, thank you, though, I guess Iā€™ll need to find that out for myself sooner or later and Iā€™ll sure do post back here if/when I do.

1 Like

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:

Letā€™s say you have a call on the client like following

Meteor.call('myMethod', (err, res) => alert(res))

with an async function you can define the method like that:

const myPromise = () => new Promise((resolve, reject) => resolve('hi'))

Meteor.methods({
  myMethod: async () => {
    const msg = await myPromise()

    return msg
  }
})

This essentially allows to write your code in a ā€œsynchronousā€ style, while still staying async by awaiting promises.

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