After a few months consulting on the rewriting of a large-scale application, I've come to realize that async
/await
was used de facto for most asynchronous operation and parallel executions seemed to be out of the picture. For example, consider this Vue code snippet:
async initStore(query) {
await this.getConfig();
await this.getUser();
await this.checkRussianContext(query);
await this.getBasket(this.$store.state.config.selectedCurrency),
await this.$store.dispatch('options/fetchOptions', {
basket : this.$store.state.basket,
});
},
Here, each line of code is executed when its predecessor is completed. Meaning getUser
will wait for getConfig
to finish fetching data before being executed.
Here are a few points that come to mind when seeing this snippet:
- What if one line does not need data from the previous one? Why block its execution and slow down our application?
- Could we run unrelated methods in parallel using something like
Promise.all
? - Related methods should probably be using a
then
block to avoid blocking the rest of the method
The point this article will be to help you catch this code smell by showing you that using async
/await
by default in some cases can have a drastic impact on performance and UX.
Unrelated queries should be executed in parallel
Let's see some concrete data, shall we?
Here's the code snippet we'll be analyzing:
const getUserData = async () => {
// Get a random dog as our user's avatar
const res = await fetch('https://dog.ceo/api/breeds/image/random')
const { message } = await res.json()
// Get our user's general data
const user = await fetch('https://randomuser.me/api/')
const { results } = await user.json()
// ...
}
Running this snippet 100 times on fast 3G (using Chrome's dev tools), the average execution time is 1231.10ms.
But why block the second query when it doesn't need the result of the first? Let's change our code to the following and re-run it 100 times.
const getUserDataFaster = async () => {
// Execute both requests in parallel
const [res, user] = await Promise.all([
fetch('https://dog.ceo/api/breeds/image/random'),
fetch('https://randomuser.me/api/')
])
const [{ message }, { results }] = await Promise.all([res.json(), user.json()])
// ...
}
We now have an average execution time of 612.50ms, half the time needed when both queries were executed one after the other.
The point is: if you can execute time-consuming queries in parallel, do it.
Try it out yourself on this codepen.
Unrelated code should not have to wait
Let's take my first example but with a twist:
async initStore(query) {
await Promise.all([
this.getConfig(),
this.getUser(),
this.checkRussianContext(query)
])
await this.getBasket(this.$store.state.config.selectedCurrency),
await this.$store.dispatch('options/fetchOptions', {
basket : this.$store.state.basket,
});
await initBooking()
},
Here, the first 3 requests are executed in parallel, whereas the next ones rely on data fetched beforehand and will therefore be executed afterwards. Although this snippet poses a problem, did you spot it?
Poor little initBooking
will have to wait for both getBasket
and fetchOptions
to finish before executing even though it has nothing to do with the data they'll fetch.
An easy solution is to trade the await
with a simple then
block.
async initStore(query) {
await Promise.all([
this.getConfig(),
this.getUser(),
this.checkRussianContext(query)
])
this.getBasket(this.$store.state.config.selectedCurrency).then(async () => {
await this.$store.dispatch('options/fetchOptions', {
basket : this.$store.state.basket,
});
})
await initBooking()
},
This way, both getBasket
and initBooking
will be executed alongside one another.
Want to see it for yourself? Check out this codepen illustrating my example.
I'll stop the article there so I don't overload you with examples, but you should get the gist of it by now.
async
/await
are wonderful additions to the Javascript language but I hope you'll now ask yourself if they have their place in the specific method you're working on and more importantly: if some of your queries could be executed in parallel.
Thank you for reading, I'd love it if you gave me a follow on Twitter @christo_kade, this way we'll get to share our mutual skepticism towards awaits
❤️
Latest comments (39)
async/await is more often used when it should not be.
The async/await control flow is an abomination to me. I already have beef with Promises, and async/await just takes it to far.
I have fixed too many bugs in other people's code because they do not want to deal with async programming and instead just try to async-await their way through a feature.
Observables > Promises > Async/Await
yea but it still in implementation progress, kinda experimental.
developer.mozilla.org/en-US/docs/W...
This post is basically explaining how async/await works. Anyone with basic knowledge of async/await knows this. The title just seems like a clickbait to me.
That's not a fair statement. I've tried to show good practices and how to avoid certain code smells based on previous experiences.
Sorry you feel that way.
Hey this was pretty interesting.
The "danger" which you are referring is not a async/await thing. No matter which language/framework you are using, it doesn't make any sense to execute sequentially independent api calls. This applies to Java, nodejs, C or whatever language we are talking.
Christopher, great and timely reminder of the danger of async. However I may be missing something but, about your last example:
None of these calls are fetching any data, at least in the code as shown. They can all be run in parallel with
Promise.all
...It's an example, the methods in question could be fetching data.
For example
getBasket
could fill the store'sbasket
state variable needed byfetchOptions
. I simply wasn't going to show the contents of these methods as they don't seem relevant to the point I'm making.Thank you for your feedback Yawar !
The title is just a bit misleading, it's more a discussion about parellelism vs serial execution.
I agree, if you don't take the program flow into account you can horribly increase application latency.
Good post
The most overlooked caveat of
async-await
is that we have to wrap it in atry-catch
block to handle errors by catching rejected promises.I don't see why this would be considered a caveat? Thenable promises also need to explicitly declare a
Promise#catch()
to handle errors? What makes it different from wrapping async/await in try-catch?I mean it's something to be aware of and it's often overlooked and not mentioned at all in articles discussing and guiding on
async-await
.The difference between this and promises is that catch callbacks can be put specifically in any place in a promise chain. Of course, we can also do this with
try-catch
around a single statement, but that gets ugly pretty fast, especially if we want to keep the same variable scope between relatedtry-catch
blocks.Great points, it's all too easy to forget that async/await is just syntactic sugar on top of promises but still the same semantics under the hood.
First of all, thanks for the article. I really enjoyed it, but something is bugging me, though.
When you say that "unrelated code shouldn't wait...", I think that there is something we're not addressing here, which is:
Unrelated code shouldn't be in the same function.
My approach on that would be:
1 - Split each block of unrelated code into its own async function. That will keep scope smaller and easy to keep track of what is happening at that moment.
2 - The main function would end up being a point where each step (each block of unrelated code) of the process are invoked in a meaningful order.
Of course, but we must take into account use-cases where our code is running inside a hook for example (mounted, created etc.), or when dealing with legacy code.
The right thing to do would be refactoring it to fit what you mentioned, but we don't always have that option.
Absolutely, thanks for catching my mistake. Must have made it while pasting my notes !
Absolutely! But there is a problem. If one of those will fail it will break all promise.all() iteration and you will be thrown into the catch. So I if you need/want to run fully async and parallel and get all results (errors and values) regardless of the failures you should use this:
let's test it with some timeout function
output:
I agree with this. Even nodejs has it on their guide.
This is kind of funny. I was planning on writing an article this evening with something pretty close to this title, but focused on C# code.
Different points, but... still, I'll have to rethink that or at least the title.
I'd love to read your take on it either way :) sorry for beating you to it haha
Why do you put
await
insidePromise.all
array? It is not needed.Great catch, I must have made a mistake while pasting from my notes. Thanks!
When I started webdev I had a tendency to make sure everything was loaded before starting to display things. A few years later I took a look at my code, horrified..