Start using async/await instead of promises and callbacks

I don’t see a lot of people pushing for async/await style programming, so I figured I’d bring it to everyone’s attention now that Meteor 1.3 (beta) has built-in support for it.

The biggest use-case for async/await is getting rid of “callback hell” in an even more efficient way than using Promises. Take the following example:

Meteor.call('myFunction', function(error, result) {
    if(error) {
        throw error;
    }

    Meteor.call('myFunction2', result, function(error, result2) {
        if(error) {
            throw error;
        }

        // Do stuff
    });
});

With the help of async/await and the deanius:promise package, we can accomplish the same thing like so:

(async function() {
    var result  = await Meteor.callPromise('myFunction', <arg1>, <arg2>);
    var result2 = await Meteor.callPromise('myFunction2', result, <arg2>, <arg3>);

    // Do stuff
}());

Meteor.call() only supports callbacks, so we use the deanius:promise package that adds the Meteor.callPromise() method which, you guessed it, does the exact same thing as Meteor.call() except it returns a promise.

Async/await has really streamlined my code, and I highly recommend people adopt this pattern. For more information, check out the following links:

A good write-up about async/await.

An excellent video by one of the guys working on the ES7 spec. The link should skip you ahead to about 10:26 where he starts talking about async programming. He details exactly what async/await is doing behind the scenes. It’s essentially syntactic sugar for a complex generator pattern. Incidentally, this generator pattern is what Babel uses to transpile the async/await keywords. If you have time, you should watch the entire thing from the beginning.

35 Likes

Hi Knotix, this is very timely. I just published a grumpy note on the state of Promises in Meteor Which is the best promise package right now? in which I conclude that there is no support for Promises in Meteor.

But your note gives me hope. Promises seem cool; async/await (in ES7) would be even more fun. Still, I have a lot of questions:

  • Does async/await support require the (still-beta) 1.3? Can it be used/polyfilled by people who want to continue with 1.2?
  • Should you rely on the okgrow:promise library when they’re referring people to deanius? See https://github.com/okgrow/meteor-promise
  • What’s the difference between okgrow/deanius packages and the MDG promise package? How do they interrelate?
  • Can you point to any documentation for the async/await calls/api?
  • Do you have working sample code? The example in your post above is interesting, but it doesn’t show how/whether you can pass arguments to myFunction(), etc. It also doesn’t show try/catch/then/done/etc. error handling.

Many thanks.

@richbhanover

Yes. You technically can get it to work in 1.2 if you remove the ecmascript package and replace it with grigio:babel, but you there’s quite a bit of configuration involved. When I was working with it, I had issues that mysteriously resolved themselves overnight, so I’m hesitant to recommend this route. You’re much better off just using Meteor 1.3

You don’t have to. In fact, before I wrote this post, I had written my own snippet that internally uses Meteor.call(), but returns a promise. However, I figured that in order to keep this post simple and easy to reference by others, I’d use an existing package that accomplishes the same thing. In this regard, you’re free to use whatever promise package you want. (I’ll update my original post to use Deanius’ package)

Meteor’s promise package is primarily designed for server use to take advantage of Node’s “Fibers”. Fibers are specific to Node and therefore do not work on the client. Basically, they allow Node to halt execution of a function until a certain condition is met. It’s pretty much exactly what ES7’s async/await keywords do, except Node implemented them way before async/await’s spec had been ironed out.

Here’s a good write-up.
Here’s an excellent video by one of the guys working on the ES7 spec. The link should skip you ahead to about 10:26 where he starts talking about async programming. He details exactly what async/await is doing behind the scenes. It’s essentially syntactic sugar for a complex generator pattern. Incidentally, this generator pattern is what Babel uses to transpile the async/await keywords.

Nothing that’s concise enough for this post, but you should be able to use my original post as an example.

As you can see by my last example, I’m actually passing the result of the first call as the first parameter of the call to myFunction2. To make things clearer, here’s a fixed example.

(async function() {
    var result  = await Meteor.promise('myFunction', <arg1>, <arg2>);
    var result2 = await Meteor.promise('myFunction2', result, <arg2>, <arg3>);

    // Do stuff
}());

There’s no need for then/done. Execution literally awaits the result of the first call before continuing (without blocking the main thread). Think of it almost as a yield in a generator function. As for catch, you can literally wrap this code in a try/catch block, since await will throw exceptions if the Promise returned by Meteor.callPromise() gets rejected.

In essence, using async/await allows you to write your code as if it’s all procedural, synchronous code, complete with try/catch. The beauty is that it doesn’t block execution the way synchronous code does. It’s a game changer because you no longer need to worry about callbacks and promises. This is why I’m so baffled that literally no one in the Meteor community is promoting this.

If you have the time, I suggest you watch all of the video in the second link above to get a grasp for how awesome this feature is.

3 Likes

Thanks for making this thread! I use async/await for everything asynchronous in all my apps now. It’s truly the way of the future, now, and will make all of you code so much cleaner to read (and more semantically meaningful).

By the way, it’s possible to use async/await with current non-promise Meteor APIs. For example, here’s how we can promisify a Meteor.call() so that we can await on it:

function callMeteorMethod(methodName, ...args) {
    return new Promise((resolve, reject) => {
        Meteor.call(methodName, ...args, (error, result) => {
            if (error) reject(error)
            else resolve(result)
        })
    })
}


async function main() {
    let result = await callMeteorMethod('foo', 1, 2, 3)
    console.log(result)
}

main()

Now you can use callMeteorMethod everywhere, instead of Meteor.call. If you happen to use it outside of an async function, you an still handle the result as you normally would with a promise:

function notAsync() {
  callMeteorMethod('foo', 1, 2, 3)
  .then(result => console.log(result))
}

notAsync()

You may have to take certain extra care (maintaining the Fibers context) in order to implement callMeteorMethod on the server-side.

16 Likes

What a great thread! Thanks for all this good information.

@trusktr The start of this quest for Promises was that I want to use server-side libraries that return promises. Would it be possible to promisify a Meteor.Method() call from the server?

@Knotix Do you have any idea how soon the 1.3 will be available?

Thanks, all!

1.3 is probably toward the end of February.

According to these meeting notes:

https://github.com/meteor/guide/blob/master/meeting-notes.md

And @sashko also confirmed it in the Transmission podcast #1

Haven’t tried async/await yet, but looks awesome. Definitly going to try it now!

I’m interesting for this.
Could you explain to declare (client/server/both) and using with full example

// server
Meteor.methods({.............})

// client 

With caolan/async, you can do the following:

async.waterfall(
  [
    function(n) {
      Meteor.call('myFunction', function (error, result) {
        n(error, result);
      });
    },
    function(result, n) {
      Meteor.call('myFunction2', result, function (error, result2) {
        // Do stuff
        n();
      }
    }
  ], function (err) {
    // Do stuff
  }
);

I think this is cleaner because we don’t have to rely on a promise package. What might be the advantage of using async/await over this approach?

Yeah, but instead you rely on caolan/async. In ES2015, Promise is a built in feature. The difference to callbacks is that the promise encapsulates the eventual result into an object that you can pass to other functions. With promises your example looks like this:

Meteor.promise('myFunction')
  .then((result) => Meteor.promise('myFunction2', result))
  .catch((error) => {
    // Handle error
  });

Where Meteor.promise is just Meteor.call that returns a promise.
So I would say compared to the async library it’s very similar but a bit cleaner.
One thing to note is that the catch handles all errors in the promise chain.

2 Likes

Yep! It’s the same as my last example. If you have problems with the Fiber context on the server side, you might like to watch this video to learn how Meteor.bindEnvironment can help.

Oh, Meteor 1.3 is using Fibers for async functions on the server side, so no bindEnvironment hacks may no longer be needed.

1 Like

Where is Meteor.promise() coming from? That’s not in yet, right?

@dfischer

[Text to make this post at least 20 characters long]

https://atmospherejs.com/okgrow/promise

I thought I’d show another pattern we can use for promisifying things, in case anyone likes it better, with less nesting. My above example would become:

function callMeteorMethod(methodName, ...args) {
    let resolve, reject
    const promise = new Promise((a, b) => { resolve = a; reject = b })

    Meteor.call(methodName, ...args, (error, result) => {
        if (error) reject(error)
        else resolve(result)
    })

    return promise
}


async function main() {
    let result = await callMeteorMethod('foo', 1, 2, 3)
    console.log(result)
}

main()

IIRC, Promise.await and Promise.async were always available on the server.

Are await myFunction() and async function(){} now available on the client as well?

2 Likes

Yes, they are available on the client in Meteor 1.3+

1 Like

Try doing this to simplify it:

await Promise.denodeify(Meteor.call).call(Meteor, [your arguments here])

@corvid Yep, in Meteor 1.3, ES7 is available on both client and server (async functions, es6 modules, everything!).

4 Likes