DEV Community

Vinicius Kiatkoski Neves
Vinicius Kiatkoski Neves

Posted on

Yet another post about async/await and promises

This week someone asked for help on Slack while having issues with async/await and promises mixed up all together. The guy was using async/await and wanted to wait for a bunch of promises to be resolved (in parallel) before moving forward. I suggested him to take a look at Promise.all but as soon as he replied I realized he had some misconceptions about the relation between async/await and promises.

The intention of this post is to walk step by step on how both things are connected until we finally get into the problem asked on Slack. I assume you understand at least a bit of promises while I show some examples. Let's get started!


First basic example of promises usage:

function doSomething() {
  return Promise.resolve('#2');
}

const promise = doSomething().then(console.log);

console.log('#1');
Enter fullscreen mode Exit fullscreen mode

Things to highlight here:

  • doSomething function returns a promise (it is resolved already)
  • Even though we put the console.log with "#2" inside first it gets printed after "#1"

So far so good. It is how promises work so there is nothing special here.

Now let's rewrite it with async/await and see how it works:

async function doSomething() {
  return '#2';
}

const result = await doSomething();
console.log(result);

console.log('#1');
Enter fullscreen mode Exit fullscreen mode

I've just switched to async/await and the result is not the same anymore. Now we get "#2" printed before "#1" which is the expected behavior once we're telling our code to wait for doSomething before moving forward with its execution.

But how could we get the same behavior with promises? Well, we have to do the same as the code above: wait for doSomething and then console.log in the right order!

function doSomething() {
  return Promise.resolve('#2');
}

const promise = doSomething().then(data => {
  console.log(data);

  console.log('#1');
});
Enter fullscreen mode Exit fullscreen mode

Cool, but now let's mix things up a bit and see how they behave. What I mean is: half promise, half async/await!

function doSomething() {
  return Promise.resolve('#2');
}

const result = await doSomething();
console.log(result);

console.log('#1');
Enter fullscreen mode Exit fullscreen mode
async function doSomething() {
  return '#2';
}

const result = doSomething().then(console.log);

console.log('#1');
Enter fullscreen mode Exit fullscreen mode

Well, what do you thing will be printed?

The first one prints "#2" and then "#1". The second one prints "#1" and then "#2".
You will notice that they are the same examples from above and with the same results. But how can we mix async/await and promises and keep the "promises" behavior?

Let's take a look at this:

function doSomethingPromise() {
  return Promise.resolve('#2');
}

async function doSomethingAsync() {
  return '#2';
}

const somePromise = doSomethingPromise();
const someAsync = doSomethingAsync();

console.log(somePromise); // Promise { ... }
console.log(someAsync); // Promise { ... }
Enter fullscreen mode Exit fullscreen mode

When you console.log both calls you'll notice that both of them return a promise! That is the connection between async/await and promises. When you put async in front of a function you're telling that it will return a promise with the value from the return statement resolved! When you await you're waiting for the promise to be fulfilled, in this case it is the same of calling the .then from the returned promise!
Attention here: We're not handling errors so don't forget you also have .catch and try/catch to deal with.

Those were really simple examples! The question asked on Slack had the following code with it:

async function myFunction(...params) {
 const value1 = await someFunction1();
 const value2 = await someFunction2(...params);

 // The following code needs to sync
 const whatNow = async () => {
   await value1.forEach(async v1 => {
     someFunction3(v1);
     someFunction4(v1);
     value2.forEach(v2 => {
       someFunction5(v1, v2);
     });
   });
 }

 whatNow();
} 
Enter fullscreen mode Exit fullscreen mode

It is a bit more complicated from the previous examples but the problem here is asynchronous code in Javascript as each someFunctionX returns a promise. Let's go and see how far we can get from this example.

The first two statements are OK but we can do better. They are OK because they work but we are actually waiting the first promise from someFunction1 to resolve and then resolving the second one. If we would rewrite it with promises we would have the following:

function myFunction(...params) {
  someFunction1().then(value1 => {
    someFunction2(...params).then(value2 => {
      ...
    });
  });
...
}
Enter fullscreen mode Exit fullscreen mode

A better strategy is to use Promise.all which guarantees that both promises will be resolved in parallel before we move forward with our execution. Our code now looks like that:

function myFunction(...params) {
  Promise.all([someFunction1(), someFunction2(...params)]).then(([value1, value2]) => {
    ...
  });
  ...
}
Enter fullscreen mode Exit fullscreen mode

Cool, it was just a simple rewriting and we're already getting some benefits. But how could we write it with async/await? I do prefer async/await because it helps a lot the readability of our code, after all we are writing code that other people will have to read and figure out what is does!

As Promise.all returns a promise we can easily use the same strategies from the first examples, so:

async function myFunction(...params) {
  const [value1, value2] = await Promise.all([someFunction1(), someFunction2(...params)]);
  ...
}
Enter fullscreen mode Exit fullscreen mode

Wow! A way better to read now and we are taking the advantage of the parallelism.

Now we have to solve the problem of the whatNow function. What he wants is to execute all the promises in parallel as it doesn't make sense to wait for each of them to resolve and then call the next one. Let's go step by step!

The first problem is using forEach with async/await. forEach is a synchronous call which won't help us with asynchronous code (Want to know more? Read this post here). The first thing we have to keep in mind is that we have the Promise.all to help us. So we could start like that:

async function myFunction(...params) {
  const [value1, value2] = await Promise.all([someFunction1(), someFunction2(...params)]);

  const promises = value1.map(someFunction3); // We have now an array of promises

  await Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

Things are getting better now. We map over each value of value1 and return a promise from it, calling someFunction3. After that we wait until all those promises are resolved!

But our problem is a bit deeper as we have more than one function to be called for each value of value1. Let's see how it would work:

async function myFunction(...params) {
  const [value1, value2] = await Promise.all([someFunction1(), someFunction2(...params)]);

  const promises = value1.map(v1 => [someFunction3(v1), someFunction4(v1)]);

  await Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

Aaand it doesn't work anymore. The reason is that now we are trying to resolve and array within another array of promises inside. Looks like [[Promise, Promise], [Promise, Promise]]. So one strategy is to flatten it! Array.prototype.flat is in experimental mode yet so I'm going to use a polyfill. Array.prototype.flatMap would be a better option but it is also in experimental mode.

async function myFunction(...params) {
  const [value1, value2] = await Promise.all([someFunction1(), someFunction2(...params)]);

  const promises = flatten(value1.map(v1 => [someFunction3(v1), someFunction4(v1)]));

  await Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

And now we've the desired result! Let's move further!

The next issue is the forEach inside the first forEach. Let's try to apply the same strategy as before: map over value2 and return a promise from someFunction5.

async function myFunction(...params) {
  const [value1, value2] = await Promise.all([someFunction1(), someFunction2(...params)]);

  const promises = flatten(value1.map(v1 => [
    someFunction3(v1),
    someFunction4(v1),
    value2.map(v2 => someFunction5(v1, v2)),
  ]));

  await Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

Aaaand it works! Wow! It works because we're flattening the array of promises, otherwise it would be creepier than before within an array of arrays of arrays...

Now we can say we are taking full advantage of Javascript asynchronous code because we are not resolving one promise at a time to dispatch the next one, we are dispatching all of them together and waiting for the final result!

Conclusion

I hope you can now somehow understand how async/await is connected to promises: They are basically the same thing but async/await provides a syntax easier to read (aaand relies on try/catch which is the standard way of handling errors).

The example from above is a real case scenario which was brought from Slack and it has its own peculiarities. Nevertheless it relies deeply in asynchronous code to work in the best way possible which means it can become trickier if you misunderstand some concepts.

If you have any other suggestions for this code just let me know as I know there are different solutions for the same problem! Any tips that might be worth adding just drop in the comments below and I do add it here!

Gist with examples: https://gist.github.com/viniciuskneves/086632be8482e2cd842669d202414249

Top comments (5)

Collapse
 
rafaelmpessoa profile image
Rafael Menicucci Pessoa

thank u, very helpfull.

Collapse
 
vaheqelyan profile image
Vahe

Good article, Thank you

Collapse
 
gollyjer profile image
Jeremy Gollehon

Super helpful. Thanks! 🙌

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Glad that you liked it!

Collapse
 
ptasker profile image
Peter Tasker

Great article explaining the relationship between promises and async/await!