Better ways to use async functions in Javascript

Photo by Andrew Neel on Unsplash

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']

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
*/

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
*/

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' }
// ]

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'

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){
    return Promise.race(
         [
            promise,
            new Promise((_,reject)=>{
               setTimeout(reject,duration,"Too late.")
            })
         ]
    )
}

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

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!