Run asynchronous functions concurrently in Node.js
Start independent asynchronous work together, choose the right promise combinator, and limit concurrency when downstream capacity is finite.
Call independent asynchronous functions before awaiting their combined result. Promise.all() then waits for all of them to fulfill and returns results in input order. It coordinates promises; it does not create threads or start function objects for you.
Sequential versus concurrent calls
async function sequential(loadUser, loadOrders) {
const user = await loadUser();
const orders = await loadOrders();
return { user, orders };
}
async function concurrent(loadUser, loadOrders) {
const [user, orders] = await Promise.all([loadUser(), loadOrders()]);
return { user, orders };
}
Use the second form only when loading orders does not depend on the user result. Function calls are evaluated in sequence, including their synchronous prefixes, but their asynchronous I/O can overlap. Total time depends on contention and service behavior; it is not guaranteed to equal the slowest individual request exactly.
For an array, await Promise.all(ids.map(loadById)) collects one operation per ID. Pass a wrapper such as ids.map((id) => loadById(id)) when the function should not receive map's extra index and array arguments.
Choose the result policy
| Operation | When it settles |
|---|---|
Promise.all |
All fulfill, or one rejects |
Promise.allSettled |
Every input settles; returns a status record for each |
Promise.race |
The first input settles, whether fulfilled or rejected |
Promise.any |
The first input fulfills; rejects with AggregateError if all reject |
all and allSettled preserve input order in their result arrays. A rejection from all, or a winner from race, does not cancel the remaining operations. An AbortController helps only when the underlying API supports and receives its signal.
Catch errors at the level where you can make a decision: fail the entire request, retain partial results, or retry selected operations. Retry policies must account for operations that may already have completed, especially writes.
Bound the number in flight
A large Promise.all can overwhelm a connection pool, database, or remote rate limit. With an installed p-limit dependency:
import pLimit from 'p-limit';
async function loadBatch(ids, loadById) {
const limit = pLimit(4);
return Promise.all(ids.map((id) => limit(() => loadById(id))));
}
The function passed to the limiter delays the start of each operation until a slot is available. The example's four-operation limit is illustrative; choose a limit based on the downstream service and measurements. For enormous inputs, also avoid retaining every task and result in memory at once.
Concurrency is not CPU parallelism
Wrapping CPU-heavy JavaScript in an async function does not move it off the event-loop thread. Several such functions can still block one another. Use worker threads or separate processes when the measured bottleneck is CPU execution. Ordinary asynchronous network and filesystem operations often benefit from overlap without that additional machinery.
See Promise.all and p-limit.
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan