JavaScript Promise.all(): Concurrency, Failures, and Better Patterns

Promise.all() is a JavaScript concurrency tool for a simple but common requirement: several asynchronous operations are all required before the next step can continue. It accepts an iterable of promises or values and returns one promise. That returned promise fulfills with an array of results when every input fulfills, and it rejects when any input rejects.

The API is small, but the design decisions around it are not. Developers need to understand when operations truly can start together, how result ordering works, why one rejection changes the aggregate outcome, and why rejection does not cancel work already in progress. Most Promise.all() bugs come from misunderstanding those semantics rather than from syntax.

Promise.all() coordinates promises that are all required

Imagine a page that needs a user profile, account permissions, and configuration before it can render. If those requests do not depend on one another, starting them sequentially wastes time: the second waits for the first even though it could already be running. Promise.all() lets the application start the independent work and then await the combined result.

The important phrase is “all required.” If partial success is useful, another combinator may be better. Promise.all() expresses a contract: the combined operation succeeds only when every required input succeeds.

Result order follows input order, not completion order

One promise may resolve in 20 milliseconds and another in two seconds, but the fulfillment array is ordered according to the input iterable. That makes destructuring predictable: the first variable maps to the first promise supplied, not the first network response to arrive. This is especially useful when combining requests for different resources.

Non-promise values are accepted as well. JavaScript effectively treats them as already fulfilled values inside the aggregate. An empty iterable produces an already fulfilled promise, while a non-empty iterable is still settled asynchronously even if all of its values are already resolved. These details matter in reusable utility functions and tests.

Concurrency is not the same thing as parallel execution

Promise.all() does not create threads and does not make synchronous JavaScript execute in parallel. It coordinates asynchronous work that has already been started or represented by promises. Network requests, timers, and other host-managed operations can make progress concurrently while the JavaScript event loop continues processing tasks.

This distinction prevents a common performance mistake. Wrapping CPU-heavy synchronous functions in promises does not automatically distribute them across CPU cores. If the workload is computational rather than asynchronous I/O, workers or another execution model may be required.

Fail-fast rejection is useful when partial success has no value

The promise returned by Promise.all() rejects when the first input promise rejects. That lets callers handle a required-set failure in one place instead of checking every result independently. It works well for groups such as “load all required startup data” or “complete every validation before committing the next step.”

Fail-fast does not mean the other operations are cancelled. A request already sent to a server may keep running after the aggregate promise rejects. If cancellation matters, the application must implement it explicitly, for example with cancellation primitives supported by the underlying operation. This is important for expensive requests, uploads, or work that changes remote state.

Use allSettled(), any(), or race() when the success contract is different

Promise.allSettled() waits until every input settles and returns the status of each one. It is appropriate when the application wants a complete outcome report even if some tasks fail. Promise.any() fulfills when the first input fulfills and rejects only if they all reject. Promise.race() settles with the first input to settle, whether that is fulfillment or rejection.

These methods are not stylistic alternatives; they describe different product behavior. A dashboard loading independent widgets may prefer all-settled behavior. A redundant request to several equivalent endpoints may prefer first-success behavior. A timeout wrapper may use race behavior. Choosing the method should start with the desired failure semantics.

Do not accidentally serialize work that could run together

A subtle anti-pattern is awaiting each operation before building the array: Promise.all([await a(), await b()]). By the time Promise.all() receives those values, the operations have already been awaited sequentially. Instead, call the async functions without awaiting each one, pass the returned promises into the aggregate, and await the aggregate once.

The same reasoning applies to loops. If every iteration is independent, collecting promises and awaiting them as a group can reduce total wall-clock time. If each step depends on the previous result, sequential awaits are correct. Concurrency should follow dependency structure, not a desire to make every function “faster.”

Unlimited concurrency can overwhelm APIs, databases, and browsers

Starting ten independent requests together may be sensible. Starting fifty thousand at once may exhaust memory, trigger rate limits, saturate connection pools, or overload a downstream service. Promise.all() itself does not provide a concurrency limit, so large workloads often need batching, a queue, or a worker-pool pattern.

That is also why reliable JavaScript testing should cover slow responses, partial failures, retries, and ordering rather than only the all-success case. Concurrency bugs frequently appear under timing conditions that simple unit fixtures do not reproduce.

Design the async boundary before choosing the promise combinator

Before writing Promise.all(), list the operations and ask which ones depend on earlier results, which ones are optional, what failure should stop the whole workflow, and whether any work must be cancelled or retried. That small design step turns a promise combinator from a convenient shortcut into an explicit reliability decision.

For code review, check four things: whether the async functions are actually being invoked before aggregation, whether result positions are mapped correctly, whether failure behavior matches the product requirement, and whether the number of simultaneous operations is bounded appropriately. When those decisions are correct, Promise.all() is one of the clearest ways to express “start these independent tasks, then continue only when all required results are ready.”

Error handling should preserve context. If three required requests run together and one fails, a catch block that reports only “something went wrong” wastes the information needed for recovery. Wrap operations with meaningful error types or include enough context to identify which dependency failed, while avoiding sensitive data in logs. If the caller can retry safely, make the retry scope explicit rather than repeating unrelated successful work without reason.

Another design question is whether the tasks are idempotent. Concurrent reads are usually straightforward, but concurrent writes can interact. Two updates may race, a later operation may depend on an earlier side effect, or retrying the aggregate may create duplicate records. Before grouping write operations in Promise.all(), verify that they are independent or that the remote API provides transaction, idempotency-key, or conflict-control behavior that makes concurrency safe.

Production code also needs timeouts. A promise that never settles can hold an aggregate open indefinitely. Fetch-based workflows often pair an AbortController or another cancellation mechanism with a timeout policy, then decide whether a timed-out task should fail the entire set or be treated as optional. Promise.all() does not answer that question for you; it only aggregates the promises it receives.

Finally, measure before optimizing. Sequential code can be clearer when operations are dependent or cheap, and excessive concurrency can make tracing harder. Use Promise.all() where independent latency can overlap and the all-required failure contract is correct. That combination—clear dependencies, bounded concurrency, explicit cancellation, and useful error context—is what turns a small API into a reliable asynchronous pattern.

When mapping over data, pay attention to what the callback returns. Promise.all(items.map(async item => ...)) works because an async callback returns a promise for each item. But if a callback starts asynchronous work and forgets to return the promise, the aggregate may appear to complete before the real work is done. This bug is easy to miss because the side effects can still finish later in development environments.

It is also useful to separate aggregation from recovery. A caller can catch an aggregate rejection and decide whether to retry the entire group, retry only the failed dependency, fall back to cached data, or surface an error. That policy belongs at the level that understands the business consequence. Promise.all() reports the contract failure; application code decides what recovery means.

For observability, give concurrent operations distinct names or correlation data. If several API calls start at once and the aggregate fails, logs should reveal which operation rejected and how long the others took. That becomes especially important in server-side JavaScript where one request may fan out to many downstream services and a single generic stack trace does not explain the distributed failure.

A final review point is readability. Concurrency code becomes difficult to maintain when a single Promise.all() contains unrelated expressions, hidden side effects, and nested callbacks. Name the promises or the operations when that makes dependencies clearer, and keep the aggregate close to the decision it represents. Clear structure makes failure behavior easier to reason about during incident debugging.

There is also an observability angle to concurrency. When several promises run together, logs should preserve enough context to identify which operation started, which failed, and which completed after another rejection caused the aggregate promise to fail. Without per-operation identifiers, concurrent work can make production incidents harder to reconstruct even when the code is logically correct.

  • img