Apps Artificial Intelligence Cloud Computing CSS Cybersecurity Data Science Database Go JavaScript Linux Python Rust Software Engineering Web Development

Choosing JavaScript Promise Combinators: all, allSettled, any, and race

4 min read .
Choosing JavaScript Promise Combinators: all, allSettled, any, and race

JavaScript provides several Promise combinators that look similar but encode very different failure policies. Choosing between Promise.all, Promise.allSettled, Promise.any, and Promise.race is less about syntax and more about defining what “success” means for a group of asynchronous operations.

The important question is not “which one runs in parallel?” Promises usually start when you create them. The combinator decides how to observe their outcomes.

Promise.all: every result is required

Use Promise.all when the operation is successful only if every input fulfills.

const [profile, preferences] = await Promise.all([
  fetchProfile(userId),
  fetchPreferences(userId),
]);

The returned promise fulfills with results in input order, regardless of completion order.

If any input rejects, Promise.all rejects with that reason. Other operations are not automatically cancelled; they may continue running in the background.

Good use cases

  • loading independent inputs required to render a result;
  • performing read-only requests where one missing result makes the aggregate unusable;
  • awaiting several independent setup operations.

Be cautious with non-idempotent side effects. If three writes start and the second rejects, the first and third may still succeed. Promise.all does not provide transaction semantics.

Promise.allSettled: inspect every outcome

Use Promise.allSettled when partial success is meaningful and you need every result.

const results = await Promise.allSettled([
  refreshSearchIndex(),
  refreshCache(),
  sendMetrics(),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("value:", result.value);
  } else {
    console.error("reason:", result.reason);
  }
}

The aggregate promise fulfills after every input settles. Each result records either status: "fulfilled" and value, or status: "rejected" and reason.

This is useful for batch jobs, dashboards, and cleanup operations where one failure should not hide the outcome of other work.

The trade-off is that failures no longer propagate automatically. Callers must inspect results and decide whether the overall operation should still be considered successful.

Promise.any: the first successful result wins

Promise.any fulfills as soon as one input fulfills:

const response = await Promise.any([
  fetchFromMirror(primaryUrl),
  fetchFromMirror(secondaryUrl),
]);

Rejections are ignored while another input might still succeed. If every input rejects, the aggregate rejects with an AggregateError.

This is a good fit for redundant sources that can return equivalent data.

It is a poor fit when “first success” hides meaningful differences between sources. If mirrors have different freshness, authorization, or consistency guarantees, racing them can return a technically successful but undesirable result.

Promise.race: the first settlement wins

Promise.race settles with the first input to settle, whether that outcome is fulfillment or rejection.

It is commonly used to represent a deadline:

function delayReject(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("timeout")), ms);
  });
}

const result = await Promise.race([
  doWork(),
  delayReject(2_000),
]);

This stops waiting after the timeout. It does not stop doWork().

For APIs that support cancellation, prefer coupling the deadline with AbortController or another cancellation mechanism so the losing operation can release network, CPU, or other resources.

Combinators do not create a concurrency limit

This pattern creates every promise immediately:

await Promise.all(items.map(processItem));

If items contains 50,000 records, you may start 50,000 operations at once. That can exhaust sockets, database connections, memory, or upstream rate limits.

Promise combinators coordinate outcomes; they are not worker pools.

For large collections, use a bounded-concurrency strategy: split work into batches, use a small worker queue, or use a library whose concurrency behavior you understand.

Result order and completion order are different

Promise.all and Promise.allSettled preserve input order in their result arrays.

If task C finishes before task A, the array still places A’s result first when A was the first input.

This is useful for pairing results with source items, but it means the output array is not an event log. If completion order matters, record completion timestamps or process results as they settle.

Avoid accidental sequential execution

These two forms have different behavior.

Sequential:

const profile = await fetchProfile(userId);
const permissions = await fetchPermissions(userId);

Potentially concurrent:

const [profile, permissions] = await Promise.all([
  fetchProfile(userId),
  fetchPermissions(userId),
]);

The second form is appropriate only when the second operation does not depend on the first result.

Do not optimize away sequencing when dependencies exist merely to reduce latency.

Handle empty inputs deliberately

Promise combinators have different behavior for empty iterables:

  • Promise.all([]) fulfills with [];
  • Promise.allSettled([]) fulfills with [];
  • Promise.any([]) rejects with an AggregateError;
  • Promise.race([]) remains pending.

That last behavior can create a request that never settles if an empty candidate list reaches a racing helper unexpectedly. Validate inputs when an empty set is not meaningful.

Common pitfalls

Assuming rejection cancels sibling work

It does not. Cancellation is separate from observation.

Using allSettled and ignoring rejected entries

That silently converts failures into apparent success. Define an aggregate policy after inspecting outcomes.

Using race as a timeout without cancelling work

The caller stops waiting, but the underlying operation can continue consuming resources.

Launching unbounded work with .map

A combinator is not a concurrency limiter.

Choose based on the business rule

A concise mental model is:

  • all: every operation must succeed;
  • allSettled: every outcome matters;
  • any: one successful operation is enough;
  • race: the first outcome of any kind decides.

Once the failure policy is explicit, the correct Promise combinator is usually obvious. Then handle the separate concerns—cancellation, concurrency limits, side effects, and observability—rather than expecting the combinator to solve them implicitly.

Related Posts

chevron-up