- Published on
How Concurrent is Promise.all in NodeJS
Introduction
Promise.all
itself doesn't introduce concurrency—it merely waits for the promises to resolve. It's the nature of the underlying promises that determines how they are executed (whether concurrently or not).
Table of Contents
Promise.all
Actually Works
How Promise.all
takes an iterable (usually an array) of promises and returns a single promise that resolves once all of the promises in that iterable have resolved. If any of the promises reject, it immediately rejects with the reason from the first promise that fails.
However, Promise.all
does not inherently handle concurrency or parallelism. It simply accepts the promises that you pass to it. If these promises start executing concurrently (like in the case of I/O-bound tasks such as network requests), it's because the tasks themselves are asynchronous and start running as soon as they're created, not because of Promise.all
.
Network Requests
When you make network requests or perform any asynchronous I/O operations, they naturally run concurrently. Promise.all
waits for all of them to complete, but it doesn't schedule or control their execution:
const fetchData = async () => {
const api1 = fetch('https://api.example.com/data1')
const api2 = fetch('https://api.example.com/data2')
const api3 = fetch('https://api.example.com/data3')
try {
const [data1, data2, data3] = await `Promise.all`([api1, api2, api3])
console.log('Data fetched:', data1, data2, data3)
} catch (error) {
console.error('Error fetching data:', error)
}
}
fetchData()
In the above example, as soon as the three fetch calls are made, they start fetching data concurrently (since network I/O is non-blocking). Promise.all
simply waits for all these promises to resolve but doesn't control how they are executed.
Promise.all and Parallelism
JavaScript, being single-threaded, does not provide native multithreading for CPU-bound tasks. So, if you want parallelism for CPU-heavy operations, Promise.all
won't help. Tasks like image processing, large computations, etc., will still run on the main thread, blocking other operations unless you offload them to Web Workers or child processes.
For I/O-bound operations (e.g., file reading, HTTP requests), JavaScript's event loop and runtime (Node.js or browsers) will run them concurrently if they are asynchronous, and this is what makes Promise.all
appear "concurrent." But it's the async nature of the tasks, not Promise.all
itself, doing the work.
Example: Coffee Shop Order Processing
In a coffee shop, we need to process orders concurrently. Baristas are responsible for processing orders. Each order takes some time to complete. Here, for breviety, we'll assume that each order takes of 5 to 10 seconds to complete.
Coffee Shop Order Processing
Orders: Each order represents a task or operation in Node.js.
Baristas: The number of baristas represents the concurrency level, simulating how many tasks Node.js can process simultaneously.
Order Queue: Orders are processed based on the number of available baristas (concurrency level).
You can add orders (tasks) to the queue using the "Add Order" button. The concurrency level (number of baristas) can be adjusted using the slider (1-5).
When you click on the "Process Orders" button, the simulation starts:
- Orders are processed simultaneously up to the concurrency limit.
- Each order takes 5-10 seconds to complete.
- Active orders (being processed) have a green progress bar.
- Orders in the queue show a "waiting" status.
As orders are completed, new orders from the queue start processing. The simulation continues until all orders are completed.
Performance Consideration: Too Many Promises
While Promise.all
can handle multiple promises, running too many promises concurrently (e.g., making thousands of API calls at once) can exhaust system resources.
In such cases, limiting concurrency with tools like p-limit
or manually batching promises is a better approach:
const pLimit = require('p-limit')
const limit = pLimit(2)
const tasks = [
() => fetchDataFromAPI('https://api.example.com/data1'),
() => fetchDataFromAPI('https://api.example.com/data2'),
() => fetchDataFromAPI('https://api.example.com/data3'),
]
const limitedPromises = tasks.map((task) => limit(task))
;`Promise.all`(limitedPromises).then((results) => {
console.log(results)
})
Promise.all
itself does not handle concurrency; it simply waits for promises to resolve. The concurrency comes from the asynchronous nature of the operations you pass to it, like HTTP requests, file I/O, etc.
If those operations can be performed concurrently, they will, and Promise.all
will wait for all of them to finish before proceeding.