On this page
Several independent asynchronous tasks rarely need the same failure policy. A page may require every configuration file, tolerate missing thumbnails, accept the first available mirror, or enforce a deadline. JavaScript's promise combinators support these different policies, but choosing by familiarity alone often creates partial results or hidden background work.
Decide what completion means
Write the business rule before selecting a method. Does the operation succeed only when every input succeeds? Should the caller receive every outcome? Does the first settled task matter, or only the first successful one? Those questions distinguish the four standard combinators.
Use the API latency tester on a public, non-sensitive endpoint to observe that request timing varies between runs. Treat it as an exploratory measurement, not proof of your application's end-to-end behavior. This variability explains why completion order should not define your data model accidentally.
Promises describe eventual results. Creating a promise does not imply that a combinator controls when its underlying work starts. Calling several fetch functions before Promise.all already starts those requests; the combinator observes their results.
Use all when every result is required
Promise.all fulfills with values in input order once all inputs fulfill. If any input rejects, the aggregate promise rejects without waiting to return the other values. This fail-fast behavior does not cancel the other operations.
async function readJson(url: string): Promise<unknown> {
const response = await fetch(url);
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}
const [profile, preferences] = await Promise.all([
readJson("/data/profile.json"),
readJson("/data/preferences.json"),
]);Use this when rendering needs both resources. Sequentially awaiting profile before starting preferences would introduce an unnecessary dependency. Conversely, if preferences require an identifier from profile, the dependency is real and cannot be removed by wrapping unrelated expressions in an array.
The MDN Promise.all reference documents ordering and rejection behavior. An empty input fulfills with an empty array. Include that case when your input list is generated dynamically.
Use allSettled for independent outcomes
Promise.allSettled waits until every input settles and returns a tagged result for each. It is useful for optional assets, bulk inspection, or any workflow that must report successes and failures together. It does not decide whether partial success is acceptable; your application must do that.
const outcomes = await Promise.allSettled([
readJson("/data/primary.json"),
readJson("/data/secondary.json"),
]);
for (const outcome of outcomes) {
if (outcome.status === "fulfilled") {
console.log("Loaded", outcome.value);
} else {
console.warn("Unavailable", outcome.reason);
}
}This preserves per-task outcomes without wrapping every task in a catch that returns an ambiguous undefined. Keep an association between each result and its originating item, usually through the same index or an explicit identifier captured in your task list.
Avoid using allSettled as a universal error suppressor. A required configuration failure should still prevent dependent behavior. A report that says three of four succeeded is useful only if the user understands what the missing fourth item means and how to recover it.
Distinguish first settlement from first success
Promise.race adopts the first input to settle, whether fulfilled or rejected. Promise.any fulfills with the first fulfillment and rejects with AggregateError only when every input rejects. These semantics differ sharply when one mirror fails quickly and another succeeds later.
Use race when the earliest outcome itself matters. Use any when interchangeable sources can satisfy the same request and failures should not win prematurely. The MDN Promise.any reference describes the aggregate failure case and empty-input behavior.
Neither method cancels losers. Racing a fetch against a timer only stops waiting through that returned promise; it does not necessarily stop the network request. Use explicit cancellation where supported and clear timers when they are no longer needed.
A subtle edge case is an empty race, which remains pending indefinitely. An empty any rejects. Validate generated task lists before relying on either method, especially when filtering can remove every candidate.
Limit concurrency and test observable behavior
Promise.all over thousands of already-started tasks does not provide concurrency control. If starting every task is undesirable, keep work as functions and let a fixed number of workers pull tasks from a queue. The limit governs starts, not merely how many promises you await at a time.
Pick a limit based on the service contract and practical measurements, not a universal magic number. For small browser workflows, a straightforward sequential loop may be clearer than building a scheduler. For large batches, expose progress and cancellation alongside the worker pool.
The MDN promise guide is a useful reference for error propagation and chaining. Remember that forEach does not await async callbacks. Use a for-of loop for sequence, or map into a deliberately chosen combinator for independence.
Test with deferred promises so you control fulfillment order. Resolve the second input before the first and verify output order. Reject an early input and check whether other operations continue. Verify empty inputs and partial success messages.
Keep these examples in your JavaScript learning collection, and record the chosen completion policy with the wider development documentation. A reviewer should be able to explain why one failed task rejects the whole operation, remains visible in a summary, or is ignored because another source succeeded. That explanation matters more than saving a line of code.
Work through a three-task timeline
Imagine three tasks started together: A fulfills after roughly 100 milliseconds, B rejects after 50, and C fulfills after 200. Promise.all rejects when B rejects, although A and C continue working. Promise.allSettled waits for C and returns three outcomes in A, B, C input order. Promise.race rejects with B's reason. Promise.any ignores B's rejection and fulfills with A's value. These illustrative timings make the policies concrete without suggesting that real networks are predictable.
Now suppose the tasks upload three independent documents. AllSettled can support a summary with two successes and one retry action, but it cannot undo the successful uploads. Likewise, all rejecting early does not create a transaction. If the business rule requires all-or-nothing effects, design that guarantee in the service protocol instead of relying on promise aggregation.
Keep task identity attached to retries. Retrying only failed items should not accidentally resubmit successful writes. If a worker pool is involved, record which tasks were started, which settled, and which remained queued when cancellation occurred. Those categories affect both progress reporting and safe recovery.
In tests, replace the illustrative timers with manually controlled deferred promises. Release B first, inspect the aggregate outcome, then release A and C and verify the remaining side effects. This exposes the important distinction between an aggregate promise settling and the underlying workflow actually becoming idle.
Key takeaways
- Choose a combinator by its success and failure policy.
- Promise.all preserves input ordering and rejects early.
- allSettled returns every outcome without deciding product policy.
- race selects first settlement; any selects first fulfillment.
- Add cancellation and start limits separately.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor