DEV Community

Aditya Bhattad
Aditya Bhattad

Posted on • Updated on

Better ways to use async functions in Javascript

If you are into web development, there is a 100% chance you have used at least some async functions. There are different ways to use async functions, such as .then() and async/await. But what if I told you there are even better ways to use async functions that can reduce request times by up to half? 🤯
Yes, it's true! The JavaScript runtime provides various features that we do not commonly know or use. One of these features is the static methods for the Promise class.
In this short blog post we will look at how we can use these to improve our async function calls.

Promise.all()

The Promise.all() method takes an iterable of promises as an input and returns a single promise that resolve to the array of results of the input promises. It rejects immediately if any input promises reject or if a non-promise throws an error, and will reject with the first rejection method.

Here is an example:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve,reject)=>{
     setTimeout(resolve,100,'foo');
})
Promise.all([promise1,promise2,promise3]).then((values)=>{
     console.log(values);
})
// expected output Array [3,42,'foo']
Enter fullscreen mode Exit fullscreen mode

Now let's see how we can use it to speed up our async calls:

Sequential vs Concurrent Execution

Normally, when making asynchronous function calls one after another, each request is blocked by the request before it, this pattern is also known as "waterfall" pattern, as each request can only begin once the previous request has returned some data.

Sequential Execution Pattern.

// Simulate two API calls with different response times
function fetchFastData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Fast data");
    }, 2000);
  });
}

function fetchSlowData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Slow data");
    }, 3000);
  });
}


// Function to demonstrate sequential execution
async function fetchDataSequentially() {
  console.log("Starting to fetch data...");

  const startTime = Date.now();

  // Start both fetches concurrently
  const fastData = await fetchFastData();
  const slowData = await fetchSlowData();

  const endTime = Date.now();
  const totalTime = endTime - startTime;

  console.log(`Fast data: ${fastData}`);
  console.log(`Slow data: ${slowData}`);
  console.log(`Total time taken: ${totalTime}ms`);
}
fetchDataSequentially()
/*
expected output:
Starting to fetch data...
Fast data: Fast data
Slow data: Slow data
Total time taken: 5007ms
*/
Enter fullscreen mode Exit fullscreen mode

Here is a diagram for better visualization
sequential execution

Using Promise.all() we can fire off all the requests at once and then wait for all of them to finish, this way as the requests do not have to wait for the previous one to finish they can start early and hence get resolved early. Promise.all() returns an array with resolved promises once all the promises passed to it are resolved.
Here is how we can improve our fetchData function using promises.

Concurrent Execution Pattern

async function fetchDataConcurrently() {
  console.log("Starting to fetch data...");

  const startTime = Date.now();

  // Start both fetches concurrently
  const fastDataPromise =  fetchFastData();
  const slowDataPromise =  fetchSlowData();

  // Wait for both promises to resolve
  const [fastData, slowData] = await Promise.all([fastDataPromise, slowDataPromise]);

  const endTime = Date.now();
  const totalTime = endTime - startTime;

  console.log(`Fast data: ${fastData}`);
  console.log(`Slow data: ${slowData}`);
  console.log(`Total time taken: ${totalTime}ms`);
}
/*
expected output:
Starting to fetch data...
Fast data: Fast data
Slow data: Slow data
Total time taken: 3007ms
*/
Enter fullscreen mode Exit fullscreen mode

Here is an diagram for better visualization

concurrent execution

We pass all the promises in an array to Promise.all() and then await it. If there are multiple requests, we can save a lot of time this way.

There is one thing to consider, though: what if one promise rejects? If we use Promise.all() in this case, it will only reject with the rejected promise. What if we want to get result of all the promises be it resolved or rejected??
To handle this case, we can use Promise.allSettled(). Let's learn about it as well.

Promise.allSettled()

It is a sligt variation of Promise.all(), the difference is Promise.allSettled() always resolves, whether the promises passe to it resolves or rejects, it returns with the array containing the results of the promises passed to it.

Example:

const promise1 = Promise.reject("failure");
const promise2 = 42;
const promise3 = new Promise((resolve) => {
    setTimeout(resolve, 100, 'foo');
});
Promise.allSettled([promise1, promise2, promise3]).then((results) => {
    console.log(results);
});
// expected output: Array [
//   { status: "rejected", reason: "failure" },
//   { status: "fulfilled", value: 42 },
//   { status: "fulfilled", value: 'foo' }
// ]
Enter fullscreen mode Exit fullscreen mode

Another useful static method is Promise.race(), it can be used to implement timeouts for async functions. Let's see how:

Promise.race()

The Promise.race() method returns a promise that fulfills or rejects as soon as any one of the promise passed to it in an array, fulfills or rejects, with a value or reason for the rejection of the promise.

Example

const promise1 = new Promise((resolve,reject)=>{
     setTimeout(resolve,500,'one');
})
const promise2 = new Promise((resolve,reject)=>{
     setTimeout(resolve,100,'two');
})
Promise.race([promise1,promise2]).then((value)=>{
     console.log(value);
     // Both resolves but promise2 is faster.
})
// expected output: 'two'
Enter fullscreen mode Exit fullscreen mode

Let's see how we can use it to implement timeouts:

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Fast data");
    }, 6000);
  });
}

const fetchDataPromise = fetchData();
function fetchDataWithTimeout(promise, duration) {
  let timeoutId;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error(`Operation timed out after ${duration} ms`));
    }, duration);
  });
  return Promise.race([
    promise,
    timeoutPromise
  ]).finally(()=>{
/*
 As pointed out by @vicariousv , if we do not clear the timeout, then
 even if our promise resolves before the timeout, the set timeout will
 still keep running. The process will end only after the timeout 
 completes, which is something to consider when using serverless 
 functionsas we pay for the runtime.
*/
     clearTimeout(timeoutId);
  })
}

fetchDataWithTimeout(fetchDataPromise,5000).then((result)=>{
  console.log(result)
}).catch((error)=>{
  console.log(error)
})
/*
expected result:
Too late
*/
Enter fullscreen mode Exit fullscreen mode

This was it for this blog post. If you want to read more about async promises as a whole, check out my other blog post on async promises: Asynchronous JavaScript: The TL;DR Version You'll Always Recall.

Thank you for reading, and I hope you learned something new!

Top comments (18)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️
// Start both fetches concurrently
  const fastDataPromise =  fetchFastData();
  const slowDataPromise =  fetchSlowData();

  // Wait for both promises to resolve
  const [fastData, slowData] = await Promise.all([fastDataPromise, slowDataPromise]);
Enter fullscreen mode Exit fullscreen mode

With the existence of Promise.all, this is definitely the "right" way of doing this, but it isn't technically the only one:

   const fastDataPromise =  fetchFastData()
   const slowDataPromise =  fetchSlowData()

   const fastData = await fastDataPromise
   const slowData = await slowDataPromise
Enter fullscreen mode Exit fullscreen mode

As you can probably tell, this approach can lead to much less readable code, which is why a solution like Promise.all makes sense.

But we can also implement our own Promise.all this way:

const simple_promise_all = async (promises) => {
   for (i in promises) {
      promises[i] = await promises[i]
   }
}
Enter fullscreen mode Exit fullscreen mode

To make the code shorter, this version just writes the results back into the same array and ignores rejected promises, but it shows how there isn't actually any magic behind Promise.all.

Collapse
 
vicariousv profile image
AndrewBarrell1

Your simple_promise_all isn't equal to Promise.all though, no?

Your's doesn't reject if any reject, and it waits for each resolve/reject sequentially, where as Promise.all tries to resolve all of them at the same time.

In your example, with 10 requests that each take approx. 2 seconds to resolve, you would need to wait 20 seconds. With Promise.all it would be 2 seconds.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Your's doesn't reject if any reject

Yes, as I pointed out, I ignored that feature to keep my example short.

and it waits for each resolve/reject sequentially, where as Promise.all tries to resolve all of them at the same time.

Both approaches resolve all of the promises at the same time, one of them just hides the implementation so you don't really think about how it's done.

In your example, with 10 requests that each take approx. 2 seconds to resolve, you would need to wait 20 seconds. With Promise.all it would be 2 seconds.

It would be 2 seconds (give or take) in both cases. awaiting a promise doesn't pause the others, so while the loop is waiting for the first one, the other promises are all still running. Once the first one finishes, the other promises will all be finished too or just about to, so awaiting them is basically just collecting their results.

Thread Thread
 
vicariousv profile image
AndrewBarrell1

You are right, I brain farted 😂

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

Just for fun, here's a more complete implementation:

const promise_all = (promises) => new Promise(async (resolve, reject) => {
   result = []

   promises
      .filter(value => value instanceof Promise)
      .each(promise => promise.catch(reject))

   for (i in promises) {
      const p = promises[i]
      if (p instanceof Promise)
         result[i] = await p
      else
         result[i] = p
   }]
   resolve(result)
})
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
richard_remer_b146e9be08d profile image
Richard Remer

If you remove the "p instanceof Promise" check it still works fine, but it also handles the case where "p" is a then-able; that is a promise that doesn't extend Promise. Await can be used on synchronous values without trouble, so no need to branch.

Collapse
 
adityabhattad profile image
Aditya Bhattad

Good point!

Collapse
 
vicariousv profile image
AndrewBarrell1 • Edited

A very important note for using timeouts with Promise.race()

When you have a timeout, while the promise may resolve or reject, the timeout will keep running, keeping the execution alive.

So if the success case passes after 1 second and the program's logic ends, the timeout set for 5 seconds will keep going for another 4 seconds and only then will the process' execution end.

This can be really important in serverless Lambdas where you pay for run time.

const timeoutRace = async (promise, ms) => {
  let timer = null

  const timeoutFn = new Promise((resolve, reject) => {
    timer = setTimeout(reject, ms)
  })

  try{
    const myRace = await Promise.race([promise, timeoutFn])

    clearTimeout(timer)

    return myRace
  } catch (error) {
    console.error("race error")
  }
}

timeoutRace(myFetchAPICall, 10000)
Enter fullscreen mode Exit fullscreen mode

In the above example, if the fetch API call succeeds after 500ms, the process ends.

Without clearing the timer as in your example, the process would continue for another 9.5 seconds before ending.

(On mobile, forgive any typos/autocorrects)

Collapse
 
adityabhattad profile image
Aditya Bhattad

Definitely something to keep it mind!

Collapse
 
davidnussio profile image
David Nussio

A better way could be using effect.website/

Collapse
 
adityabhattad profile image
Aditya Bhattad

Will definitely check it out, Thanks!

Collapse
 
milan_lietava_c6d0525f5e6 profile image
Milan Lietava

Thanks for sharing Aditya. Helped me a lot. Nice brief examples as well.

Collapse
 
andrew-saeed profile image
Andrew Saeed

I have learned a lot! Thanks, Aditya!

Collapse
 
marmeden profile image
eneasmarin

Very interesting Aditya!

Collapse
 
sufyan_syed profile image
Sufyan Syed

Recently learnt promises and was looking for practical use cases of these methods. This is the best explanation I found. Thanks a lot!!! 💛🖤

Collapse
 
yourakshaw profile image
Ayush Kumar Shaw

I definitely learned something new, Aditya! Thanks for this amazing blog post! Had to bookmark this one! Thanks again! 💯

Collapse
 
adityabhattad profile image
Aditya Bhattad

I'm really glad, you got to learn!

Collapse
 
tonny0831 profile image
tonny_x_bit

In my opinion async func is grammar in ES-6. So I think it's better method in javascript.