Asynchronous programming in JavaScript allows resource-intensive operations to run in the background without interrupting the main thread. Operations like API calls and file processes are some of the operations which should be run asynchronously.
Promises. all()
is a powerful function that can manage these operations concurrently. This article will cover how to manage multiple promises concurrently with Promise.all()
Let’s dive in.
What is a Promise
A promise is an object that represents the eventual failure or completion of an asynchronous event. Let’s look at a simple promise.
const userId = 1;
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
resolve({ name: "John Doe", email: "john@example.com" });
} else {
reject(new Error("User not found"));
}
}, 1000);
});
A promise takes a function with two parameters, resolve and reject. In our example, the promise will resolve if the operation is successful (i.e., if the userId===1
.If the operation fails the promise will be rejected.
The lifecycle of a promise starts in the pending state, and it will eventually either be fulfilled or rejected. Currently, the promise is pending. To consume the promise, we call .then()
to handle the result.
The output will either be the user data(if fulfilled) or an error(if rejected).
promise
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
Since the operation is successful, the promise will resolve.
const userId = 1;
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 1) {
resolve({ name: "John Doe", email: "john@example.com" });
} else {
reject(new Error("User not found"));
}
}, 1000);
});
promise
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
if we change the value of userId, the promise will be rejected and you will get the error User not found
Suppose you had multiple promises, you would handle each promise independently like this:
const promise1 = new Promise((resolve, reject) => resolve(1));
const promise2 = new Promise((resolve, reject) => resolve(2));
const promise3 = new Promise((resolve, reject) => resolve(3));
promise1
.then((value) => {
console.log(value);
promise2
.then((value) => {
console.log(value);
promise3
.then((value) => {
console.log(value);
})
.catch((err) => {
console.log("promise3 error", err);
});
})
.catch((err) => {
console.log("promise2 error", err);
});
})
.catch((err) => {
console.log("promise1 error", err);
});
There are a few potential issues that will arise from the execution above:
Each promise runs after the previous one is completed. promise2 will start after promise1 resolves and promise3 will start after promise2 resolves; This slows down execution.
The nested structure in the .then chaining results in “callback hell”, making the code harder to read and maintain.
Each error is handled independently which adds to more complexity.
A better approach would be to use Promise.all()
,which allows promises to run at the same time, hence improving performance and error handling
Enhancing Asynchronous Operations with Promise.all()
Promise.all()
takes an iterable of promises and returns a single promise. The syntax looks like this:
Promise.all(iterable)
If we use Promise.all()
In our earlier example, we have something like this:
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
.catch((err) => {
console.log("promise all error", err);
});
As you can see, this approach is cleaner and easier to understand.
JavaScript is a single-threaded language, meaning that each piece of code waits for the previous one to complete before going to the next.
So if JavaScript is single-threaded, how does Promise.all()
handle multiple promises?
Promise.all()
operates on the concurrency principle, which means that all the promises will start executing not necessarily at the same moment, but initiated without waiting for one to complete before starting the next.
Promise.all()
only resolves when all the promises in the iterable are fulfilled, However, if any of the promises in the iterable rejects, Promise.all()
will reject immediately and ignore the result of the remaining promises.
Practical Examples
Promise.all() excels in scenarios where you need to perform multiple independent asynchronous operations and wait for all of them to finish before proceeding.
Let’s look at some of these examples where Promise.all()
can be used to improve efficiency in real-world applications.
1.Fetching Data from multiple API’s
Consider a scenario where you are working on an application that fetches data from two different APIs simultaneously.
Let’s attempt to fetch the data sequentially from multiple API’s and also log the time taken to finish the request.
const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
const apiUrl2 = "https://jsonplaceholder.typicode.com/todos/2";
const fetchData = async () => {
console.time("fetch");
try {
const response = await fetch(apiUrl);
const data1 = await response.json();
const response2 = await fetch(apiUrl2);
const data2 = await response2.json();
console.log(data1);
console.log(data2);
console.timeEnd("fetch");
} catch (error) {
console.error(error);
} finally {
//clean up here
}
};
fetchData();
Here is the output:
The time taken to process the request is 50.36 ms. This execution time can be improved. To illustrate the benefits of concurrency, Let’s compare the approach using Promise.all()
const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
const apiUrl2 = "https://jsonplaceholder.typicode.com/todos/2";
function fetchData1() {
console.time("fetch1");
Promise.all([
fetch(apiUrl).then((response) => response.json()),
fetch(apiUrl2).then((response) => response.json()),
])
.then((results) => {
console.log("Results:", results);
console.timeEnd("fetch1");
})
.catch((error) => {
console.error("Error fetching data:", error);
});
}
fetchData1();
Here we are using Promise.all()
to run multiple asynchronous operations concurrently. Promise.all()
will take an array of promises and return a single promise when all the promises have resolved.
Here is the output.
From the output, we can see that using Promise.all()
is slightly more efficient: This improvement occurs because Promise.all()
allows both operations to start simultaneously, rather than waiting for one to finish before starting the other.
In real-world applications with more complex operations or additional API calls, the performance gains from using Promise.all()
can be even more significant.
However, if you want to wait for all the promises to settle, regardless of whether they fulfill or reject, you can use Promise.allSettled()
- Sending Multiple Chunks of Data Suppose you have an array of text chunks that represent customer feedback or reviews and you need to analyze the sentiment of each chunk by sending it to an external API for analysis.
In this case, all the data needs to be sent at the same time. In this case, you can use Promise.all()
and send all the requests concurrently, and then wait for all of them to resolve before getting the results.
For example, suppose we needed to analyse this sample data:
const data = [
"I absolutely love this product! It's amazing.",
"The service was terrible. I won't come back again.",
"Great experience overall. Would recommend to friends.",
"Beyond what I expected. The product quality is excellent.",
"Jazz performance was mind-blowing, I enjoyed every moment.",
];
In this case, all the data needs to be sent at once; sending data sequentially will be time-consuming. Instead, we will use Promise.all()
to initiate multiple API calls simultaneously.
You will have something like this:
const AnalyseReviews = async (textChunk) => {
return new Promise((resolve) => {
setTimeout(() => {
const sentiment =
textChunk.includes("love") || textChunk.includes("great")
? "positive"
: "negative";
resolve({ text: textChunk, sentiment });
}, 500);
});
};
const reviewFeedback = async () => {
try {
const results = await Promise.all(
data.map((chunk) => AnalyseReviews(chunk))
);
console.log("results:", results);
} catch (error) {
console.error("An error occurred, please try again:", error);
}
};
reviewFeedback();
3. Reading and Processing multiple files concurrently.
Suppose you have an application that accepts bulk uploads from users. After taking all the necessary measures to validate the files, you can use Promise.all()
to perform multiple file reads in parallel. This is far more efficient than reading each file one by one in a sequential manner.
Without Promise.all()
, you would have to wait for each file to be read completely before reading the next file. This would lead to more processing time, especially if you have a larger number of files.
However, with Promise.all()
, all file reading operations are initiated simultaneously, leading to considerable time savings and a great user experience.
import { readFile } from "fs/promises";
async function processFiles(filePaths) {
try {
const fileContents = await Promise.all(
filePaths.map((path) => readFile(path, "utf8"))
);
return fileContents.map((content) => content.toUpperCase());
} catch (error) {
console.error("Error processing files:", error);
throw error;
}
}
// Usage
const filePaths = ["file3.txt", "/file3.txt", "file3.txt"];
processFiles(filePaths)
.then((contents) => console.log(contents))
.catch((error) => console.error(error));
It’s also important to note that when reading many large files simultaneously, you should be mindful of potential memory considerations.
Summary
In conclusion, Promise.all()
offers a lot of benefits which are summarised below
Cleaner Code: Promise.all()
makes your code easier to understand since you don’t have nested .then() chains. Requests are handled in a single .then() block.
Efficient: By making requests concurrently, your application’s overall performance improves, as the total time required to fetch the data is reduced.
Top comments (0)