DEV Community

Cover image for Using Async/Await: The right way
Oğuzhan Olguncu
Oğuzhan Olguncu

Posted on • Updated on • Originally published at ogzhanolguncu.com

Using Async/Await: The right way

When async/await operations first introduced devs got hyped up about how things going to be clearer, shorter and faster. But, the problem was we were still writing synchronous code. Like, procedures work in order line by line but this is not how async works. In MDN Docs Async defined as Asynchronous functions operate in a separate order than the rest of the code via the event loop, returning an implicit Promise as its result. So, If you have a long-running task such as Db queries or bulky API calls this is the right way to handle. Yet, using await keyword to resolve for every long-running task can be detrimental. I'm going to demonstrate how to use async effectively. I've used Axios for requests and Performance-now for calculating the execution time.

Alt Text

Suppose we have two APIs one for Pokemon and another for Digimon. From line 7 to 11 we just want to do operations related to pokemon. Now, you may ask: Why did you make Digimon call then, Right? To show the impact of await of course. So, it is okay to put two API calls side by side that is completely okay unless you use await. The question you should always be asking is, "Do I really need data coming from API at the next line?" If, the answer is no then avoid await. Even if you are not going to use Digimon data await will try to resolve it and resolving async operations takes a toll on your program. As you can see at Terminal output this program takes 2539 ms. Now, check this out.

Alt Text

This one takes 282 ms. So the thread is still not blocked but it takes 10 times shorter time to execute. Now, let's iterate over these APIs 50 times.

Alt Text

Approximately 40 seconds. Now without Digimon await.

Alt Text

Without await, it takes 10 seconds. So the difference is 30 seconds that is not something we can unsee. If we increase the number of iterations difference will even greater.

Final Thoughts

As you can see how single await can hinder the performance of your program. Don't think sync when you are programming async. Always ask, "Do I really need that data right now?"

Thanks for reading.

Top comments (27)

Collapse
 
simonlegg profile image
Simon • Edited

This is a great write up of a common problem, I would even go one step further and suggest that for your second example could be improved even more

async function withAwaitAsyncData() {
    const responsePokemon = await axios('https://pokeapi.co/api/v2');
    const responseDigimon = axios('https://digimon-api.herokuapp.com/api/digimon');

    // do pokemon stuff

    // do digimon stuff
}
async function withAwaitAsyncData() {
    const responsePokemon = axios('https://pokeapi.co/api/v2');
    const responseDigimon = axios('https://digimon-api.herokuapp.com/api/digimon');

    await responsePokemon;
    // do pokemon stuff

    await responseDigimon
    // do digimon stuff
}

Since the request to the digimon API doesn’t rely on the pokemon API response, you could trigger them separately and only await on the Promise once you need it.

Collapse
 
jamesthomson profile image
James Thomson

Perhaps I'm misunderstanding your suggestion, but you either need to assign the result to a variable or use the Promises' chain.

e.g.

const responsePokemon = axios('https://pokeapi.co/api/v2');

await responsePokemon;

Object.keys(responsePokemon.data).forEach(el => console.log(el))

will result in a TypeError.

So you need to write it as

const responsePokemon = axios('https://pokeapi.co/api/v2');
const res = await responsePokemon;
Object.keys(res.data).forEach(el => console.log(el))

which defeats the purpose (depending on the use case, the Promise may be intended to be used later)

or

const responsePokemon = axios('https://pokeapi.co/api/v2');
await responsePokemon.then(({data}) => Object.keys(data).forEach(el => console.log(el)));
Collapse
 
ogzhanolguncu profile image
Oğuzhan Olguncu • Edited

Thanks for clarifying it. That was exactly what I was trying to say, If you don't need it that exact moment you do not have to await it. This is one of pitfalls of async/await.

Collapse
 
vinceramces profile image
Vince Ramces Oliveros

Question. Have you checked the performance using Promise.all? While this might not be the use-case. I was thinking that maybe there is a difference in speed of execution if you're going to use Promise.all.

Collapse
 
simonlegg profile image
Simon • Edited

I think there would be performance gain using Promise.all if you were comparing it to Oğuzhan’s first example.

Assuming pokemon API takes 3 seconds to respond and digimon API takes 10 seconds to respond (and that they both always succeed). Oğuzhan’s example would take 13 seconds (the sum of each response) before getting to any pokemon related logic, whereas Promise.all would bring that down to 10 seconds (the length of the longest response).

However, if you sent them off in parallel as per my example then you can get to the pokemon related logic as soon as thats returned, 3 seconds! And while you’re doing the pokemon logic, the digimon response is still coming back in time for when you need to do digimon logic.

Collapse
 
jamesthomson profile image
James Thomson

This. But instead of using Promise.all, use Promise.allSettled. Promise.all will reject everything if any one of the Promises fail, whereas allSettled will return each Promises state so you can handle the resolved and rejected ones appropriately.

Collapse
 
ogzhanolguncu profile image
Oğuzhan Olguncu

In some cases Promise.All out performs Async/Await. In most they are roughly the same.

Collapse
 
jamesthomson profile image
James Thomson

In most they are roughly the same.

Hmmm, no, I really don't think so. As Simon said above, if you call all Promises at the same time then it will only take as long as the longest one to resolve, rather than waiting for the first, then moving on to the second, etc.

Thread Thread
 
ogzhanolguncu profile image
Oğuzhan Olguncu

James, you were right. Since Promise.All was not the topic of this article but I ran some code to test it here the results.

Promise.All with loops:

Promise.All without loops:

Collapse
 
kethmars profile image
kethmars

Hey! Great writing.
Question though(maybe I missed it) - how can you be completely sure that the Digimon part will be done by the time I need it?

Collapse
 
ogzhanolguncu profile image
Oğuzhan Olguncu

Same as pokemon. Do you really think whenever you put await before your request to resolve it resolves it immediately? It still leaves the function scope and continues to execute other code. Whenever request finishes comes back to the function scope and execute the rest of the code. But, I may be mistaken.

Collapse
 
kethmars profile image
kethmars

I know that when I await something then the data will be available in the next line.

But I'm missing how, for example in the second picture, I can be sure that Digimon data is available when I read "This is Digimon" part.

Thread Thread
 
ogzhanolguncu profile image
Oğuzhan Olguncu

You need to await it on the Digimon part. Story of this article was to use await keyword when it's really necessary. Why would you want to resolve data with await when you are not going to need it?

Thread Thread
 
kethmars profile image
kethmars

You need to await it on the Digimon part.

That's the part I was missing from the article and was confused about.
In that case, I agree with what you've said. Thank you for explaining.

Thread Thread
 
ogzhanolguncu profile image
Oğuzhan Olguncu

I'm glad you deepen the conversation. Thanks.

Thread Thread
 
dlim87 profile image
Daniel Lim

Maybe I'm being obtuse, but how would you set it to await the Digimon call after it has already been kicked off?

Would that just be like const doSomething = await responseDigimon ?

Thread Thread
 
ogzhanolguncu profile image
Oğuzhan Olguncu

Actually, not the request itself takes time but resolving it does. Await basically means code is trying to resolve the request via allocating memory to const doSomething = await responseDigimon.

Thread Thread
 
jamesthomson profile image
James Thomson

I get what you're trying to say in the article, but I think the real confusion lies in that why would you do it like this. Why assign assign the axios reference to responseDigimon at the top of your function block, but use it much farther down? IMO, the const should be grouped with the await so the related logic is together. I'd also much rather break these up into separate functions so you don't get blocking code, but I guess that's another story.

Thread Thread
 
ogzhanolguncu profile image
Oğuzhan Olguncu

I've written down this article because I have seen this type of mistakes numerous time. Imagine a case where you call 5-6 responses with await without really using some of them just to be sure that async call will be in his/her sight so it can be referable some other time. Can you imagine the performance of the app? By the way, as you pointed it out it is really nonsense to call response at the top to use so much later in your code.

Thread Thread
 
jamesthomson profile image
James Thomson

Agreed, you should only call the endpoint when you need it. I think some readers just found confusion where one const used an await, but the other didn't and then it was never shown how it was meant to be used later in the code. That's all :)

Collapse
 
crimsonmed profile image
Médéric Burlet • Edited

There are moments though where you would like to be able to load all data at startup. There are a few tricks you can use to await requests on class initiation. In some cases this can be very useful.

You can also chain all requests and just await all the promises with promise.all This means the request are all run in async and you dont have to wait for one to finish.

Collapse
 
perrydbucs profile image
Perry Donham

I don't really get the point of this, if there's a dependency between pokemon and digimon, you can't just ignore that. Worse, you're just throwing away any possible response from the digimon call, so you don't see the undefineds that are going to come flying out of that loop. All you've shown is that if you skip over something long, it takes less time.

Collapse
 
nudelx profile image
Alex Nudelman • Edited

await only a syntax sugar for then
what is basically happened here, is a promise chain

this code:

const responsePokemon = await axios('https://pokeapi.co/api/v2');
const responseDigimon = await axios('https://digimon-api.herokuapp.com/api/digimon');

will be translated into this:

axios('https://pokeapi.co/api/v2')
 .then( result => axios('https://digimon-api.herokuapp.com/api/digimon'))
 .then( result => {...})

this is called a promise chain . Now it's synchronized and blocked

to solve it in parallel you should use this => twitter.com/_nudelx_/status/123739...

Collapse
 
jenc profile image
Jen Chan

Awesome! I never even thought an await may cause performance issues as I know I need the data but I just want to control the order.

Collapse
 
jankohlbach profile image
Jan Kohlbach

great to remember an hopefully an eye-opener for some people as well 👍🏽

Collapse
 
olgagnatenko profile image
Olga Gnatenko • Edited

How do we make sure that digimon data is received after withAwaitAsyncData finishes working? Assuming we need digimon data somewhere else outside the function.
Maybe a better way would be to make digimon async call out of the withAwaitAsyncData function at all? (and handle its results separately)?

Collapse
 
ogzhanolguncu profile image
Oğuzhan Olguncu • Edited

You are right about making digimon call outside of the function scope. But, that's not the context of this article.