DEV Community

Cover image for Async/await & Promise interoperability
mpodlasin
mpodlasin

Posted on

Async/await & Promise interoperability

Usually, when discussing Promises and async/await syntax, people frame it as an "either-or". You either devote to using one or the other and that's it.

But this is not at all true. Async/await was designed as a mechanism building upon (introduced earlier) Promises. It was meant as an enhancement, not as a replacement.

There are still things that are easier to do in Promise syntax. What is more, programming in async/await without understanding what is happening underneath might lead to actual inefficiencies or even errors.

So in this article we want to present Promises and async/await as mechanisms that work well together and support each other, allowing you to have a richer coding vocabulary at your disposal, making asynchronous programming easier to tame.

From async/await to Promises

So let's say you have an extremely basic function, returning some value:

function getFive() {
    return 5;
}
Enter fullscreen mode Exit fullscreen mode

It is a function that does not accept any arguments and returns a value that is a number.

For example in TypeScript, we would describe that in the following way:

function getFive(): number;
Enter fullscreen mode Exit fullscreen mode

Now what happens when you declare the very same function as async?

async function getFive() {
    return 5;
}
Enter fullscreen mode Exit fullscreen mode

You might think "well, it still simply returns a number, so the type of that function did change".

That's however false. This time it is a function that represents an asynchronous computation, even if everything in it's body is fully synchronous.

Because of that reason, it is no longer a function that simply returns a number. Now it instead returns a Promise, that itself resolves to a number.

In TypeScript syntax we would write:

function getFive(): Promise<number>;
Enter fullscreen mode Exit fullscreen mode

So let's play around with this "async" function and prove that it is nothing more than a function that returns a Promise with a number inside.

Let's first call that function and check the type of the value that gets returned:

const value = getFive();

console.log(value instanceof Promise);
Enter fullscreen mode Exit fullscreen mode

If you run this in Node.js or a browser, you will see true printed in the console. Indeed, value is an instance of a Promise constructor.

Does this mean that we can simply use then method to finally get the actual value returned by the getFive function? Absolutely!

getFive().then(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

Indeed, after running this code 5 gets printed to the console.

So what we found out is that there is nothing magic about async/await. We can still use Promise syntax on async functions (or rather their results), if it suits our needs.

What would be an example of a situation where we should prefer Promise syntax? Let's see.

Promise.all, Promise.race, etc.

Promises have a few static methods that allow you to program concurrent behavior.

For example Promise.all executes all the Promises passed to it at the same time and waits for all of them to resolve to a value, unless any of the Promises throws an error first.

Because those static Promise methods always accept an array of Promises and we said that async functions in reality return Promises as well, we can easily combine usage of async functions with, for example, Promise.all:

async function doA() {
    // do some asynchronous stuff with await syntax
}

async function doB() {
    // do some asynchronous stuff with await syntax
}

Promise.all([doA(), doB()])
    .then(([resultFromA, resultFromB]) => {
        // do something with both results
    });
Enter fullscreen mode Exit fullscreen mode

So we defined two asynchronous functions, inside of which we can use the full power of async/await.

And yet at the same time nothing stops us from using Promise.all to execute both tasks concurrently and wait for both of them to complete.

It's use cases like this, that make some people kind of wary of async/await. Note that an inexperienced programmer would probably think that he really needs to use await syntax on both of those async functions and he/she would end up with a code like this:

const resultFromA = await doA();
const resultFromB = await doB();

// do something with both results
Enter fullscreen mode Exit fullscreen mode

But this is not the same thing at all!

In this example, we first wait for the function doA to finish executing and only then we run doB. If doA takes 5 seconds to finish and doB takes 6 seconds, the whole code will take 11 seconds to run.

On the other hand, in the example using Promise.all, the code would run only 6 seconds. Because doA and doB would be executed concurrently, the whole code would only take as long as the time to wait for the last resolved Promise from an array passed to Promise.all.

So we can clearly see that being aware of both async/await and Promise syntax has clear advantages. On one hand we can get more readable, "sync-like" code. On the other we can avoid traps of async/await by using specialized functions for dealing with Promises in more nuanced ways.

From Promises to async/await

So we have seen that even when we use async/await, we can "switch" to the world of Promises with no problem.

Is it possible to do that the other way? That is, can we use async/await syntax, when dealing with Promises that were created without the use of async functions?

The answer is - of course!

Let's construct a classical example of a function that returns a Promise that resolves with undefined after given number of milliseconds:

const waitFor = (ms) => new Promise(resolve => {
    setTimeout(resolve, ms);
});
Enter fullscreen mode Exit fullscreen mode

Now - as we said - it is absolutely possible to use this classically constructed Promise in an async/await code. Let's say we want to create an async function that waits 500 milliseconds between two HTTP requests:

async function makeTwoRequests() {
    await makeFirstRequest();
    await waitFor(500);
    await makeSecondRequest();
}
Enter fullscreen mode Exit fullscreen mode

This example will work exactly as one would expect. We wait for the first HTTP request to finish, then we wait 500 milliseconds and just then we send a second HTTP request.

This shows you an example of a very practical use case, when you might first have to define a Promise wrapping some asynchronous behaviour and just then use it in a friendly async/await syntax.

What is a Promise for an async function?

Let's now ask ourselves a question: what is actually considered a Promise in that await somePromise syntax?

You might - very reasonably - think that it can be only a native ES6 Promise. That is, it can only be an instance of a built-in Promise object available in Node.js or browser environments.

But - interestingly - it turns out to be not really true.

await works on things that can be much more loosely considered a "Promise". Namely, it will work on any object that has a then property which is a function.

Weirdly, it doesn't really matter what that function does - as long as it is a function and it is under then property on the object, it's considered a Promise by the async/await mechanism.

If an await keyword gets called on an object like that, the then of that object will be called, and async/await will itself pass proper callbacks as arguments to this function. Then the mechanism will (sic!) await until one of the callbacks passed to then gets called.

This might seem complicated, so let's see it in action, step by step.

First we will create an empty object and call await on it:

const notReallyAPromise = {};

async function run() {
    const result = await notReallyAPromise;

    console.log(result);
}

run();
Enter fullscreen mode Exit fullscreen mode

If you run this snippet, you will see that an empty object - {} - gets logged to the console. That's because if an object doesn't fulfill async/await's expectations of a Promise (does not have then method), it will simply get passed through the await syntax.

Note that this happens even if we add a then property on our object, but still don't make that property a function:

const notReallyAPromise = {
    then: 5
};
Enter fullscreen mode Exit fullscreen mode

After this change, the code snippet will result with a { then: 5 } in the console.

Just as before, our object simply gets passed through the await syntax and simply gets assigned to result variable, as usual.

But now let's change then property to a function:

const notReallyAPromise = {
    then() {}
};
Enter fullscreen mode Exit fullscreen mode

This time nothing appears in console. That happens, because async/await mechanism detects that there is a function under the then property of the object. So it treats this object as a Promise: it calls then methods, passing to it proper callbacks. But because in this case we don't do anything with them, nothing happens.

Let's take the callback passed as a first argument and call it with some value:

const notReallyAPromise = {
    then(cb) {
        cb(5);
    }
};
Enter fullscreen mode Exit fullscreen mode

This time we will see 5 printed on the console. This happens, because this time we did call a callback passed by async/await mechanism. The value we called the callback with is then treated as a result from our "Promise".

If that's confusing to you, think about how you would use our notReallyAPromise object without any special syntax:

notReallyAPromise.then(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

This will also result in a 5 being printed to the console. Note how - even though our object is not an instance of a Promise constructor, using it still looks like using a Promise. And that's enough for async/await to treat such object as a regular Promise instance.

Of course most of the time you will simply use await syntax on regular, native Promises. But it is not a stretch to imagine a situation where you will use it on objects that are only "Promise-like" (often also called "thenables").

There exist libraries that use own Promise polyfills or some custom Promise implementations. For example Bluebird features custom Promise implementation that adds interesting, custom behaviors to a regular Promise.

So it's valueable to know that async/await syntax works out of the box not only with native Promises but also with a vast number of libraries, implementations and polyfills. Very often you don't have to wrap that custom code in a native Promise. You can simply use await on it, as long as this code fulfills a simple contract of having a then function, that we described earlier.

Conclusion

In this article we learned how the design of Promises and async/await syntax allows us to use both of those solutions interchangeably.

My goal was to encourage you to never just mindlessly use one solution, but rather to think about which one fits your current needs in the best way.

After all, as you just saw, at any point you can switch from one style to the other. So never feel locked to only one syntax. Expand your vocabulary to always write the cleanest and simplest code possible!

If you enjoyed this article, considered following me on Twitter, where I will be posting more articles on JavaScript programming.

Thank you for reading!

(Cover Photo by Cytonn Photography on Unsplash)

Top comments (0)