Why does `Promise.any()` reject with `AggregateError` when every input fails?
`Promise.any()` gives you the first fulfillment, but it does not convert a fully failing batch into the first rejection. This article shows why all failures are collected, when `AggregateError.errors` matters, and how to test the boundary.
Promise.any() looks like a quick way to try several sources and keep the first usable answer. The surprising part is the failure mode: if every promise rejects, you do not get one arbitrary rejection. You get a single AggregateError that represents the whole failed batch.
This article explains the contract, shows a tiny local fixture that produces both outcomes, and calls out the cases where Promise.any() is the wrong tool because you actually need per-promise failure handling.
Promise.any() optimizes for success, not failure ordering
The guarantee is simple: `Promise.any()` resolves as soon as one input fulfills. It does not wait for the rest of the batch once it has a usable value. That is why it is a good fit for duplicate sources, mirrored requests, or any scenario where the first success matters more than the slower losers.
The important part of the failure story is that the combinator is not choosing a single rejection to represent the batch. It is waiting to see whether any fulfillment still exists. Only when every branch has failed does it reject, and the rejection is the aggregate of those failures rather than one arbitrary branch.
| Combinator | What wins | What happens on total failure | When to use it |
|---|---|---|---|
| `Promise.any()` | The first fulfillment | Rejects with `AggregateError` after every input rejects | When you only need one successful result |
| `Promise.race()` | The first settled promise | Rejects or fulfills with the first settled outcome | When timing or earliest settlement matters |
| `Promise.allSettled()` | No single winner; it waits for all | Always fulfills with every result record | When you need the outcome of every branch |
SourcesMDN: Promise.any (opens a new tab)ECMAScript specification: Promise.any (opens a new tab)
Why the rejection becomes one `AggregateError`
A single error would be misleading here. The whole point of `Promise.any()` is that the input set is a search space for one usable result. If every branch fails, the caller usually wants to know that the entire search space failed, not just which branch happened to reject first.
`AggregateError` is the right shape because it preserves the per-branch reasons while still giving the caller one rejection to handle. That matters when the branches fail for different reasons: one request times out, another gets a 404, and a third is rejected by validation. The aggregate keeps those causes separate enough to inspect, but the API still has a single failure signal.
SourcesMDN: AggregateError (opens a new tab)MDN: Promise.any (opens a new tab)
Prove both outcomes with one deterministic fixture
The local test uses controlled promises instead of network calls so the article can prove the combinator boundary without depending on latency. One fixture makes a slow fulfillment arrive after an early rejection. The other fixture makes every branch reject so the code can assert the `AggregateError` shape and inspect the collected reasons.
That is the boundary the reader needs in practice. The article is not trying to benchmark the runtime or claim anything about transport reliability. It is showing how the language contract behaves when the batch succeeds and when it fails completely.
export function delayedOutcome(label, delayMs, outcome) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (outcome.type === 'fulfill') {
resolve({ label, value: outcome.value });
return;
}
reject(new Error(`${label}: ${outcome.reason}`));
}, delayMs);
});
}
export const fixture = {
firstSuccess: [
delayedOutcome('cache', 5, { type: 'reject', reason: 'cache miss' }),
delayedOutcome('primary', 20, { type: 'fulfill', value: 'primary value' }),
],
allFail: [
delayedOutcome('cache', 5, { type: 'reject', reason: 'cache miss' }),
delayedOutcome('primary', 10, { type: 'reject', reason: 'primary down' }),
],
}; When `Promise.any()` is the wrong combinator
Use a different combinator when the first outcome is not the one you care about. If you need the earliest settlement regardless of success or failure, `Promise.race()` is the right contract. If you need every result, `Promise.allSettled()` is better because it makes the whole batch visible instead of hiding it behind one success or one aggregate failure.
Another limit is error handling. `AggregateError` tells you that every branch failed, but it does not tell you how to recover. Recovery still depends on the branch context. A timeout may deserve a retry, a validation failure may need a code change, and a denied request may need a different credential or endpoint. The language gives you the bucket of reasons; your code still decides what each reason means.
- Use `Promise.any()` when one success is enough.
- Use `Promise.race()` when first settlement matters more than success.
- Use `Promise.allSettled()` when you need the full outcome list.
- Inspect `AggregateError.errors` only when all inputs fail and you need the branch reasons.
SourcesMDN: Promise.any (opens a new tab)MDN: AggregateError (opens a new tab)
What the Node test proves, and what it does not
The regression test proves two things: a fulfilled branch wins even if an earlier branch rejects, and a batch of all rejections produces one `AggregateError` with both branch reasons. That is enough to lock down the language contract for the article.
The test does not prove browser support differences, polyfill fidelity, or timeout strategy. It also does not claim that `Promise.any()` is a general retry policy. It only proves the combinator behavior that the article is explaining, which is the narrow claim worth keeping in the draft.
A worked trace makes the decision boundary visible
Imagine a cache lookup and a primary fetch that start at the same time. The cache path rejects quickly because the value is not there. The primary path resolves later with the real result. Even though the cache failed first, `Promise.any()` does not commit to that failure because there is still an unfulfilled branch that might succeed.
That is the behavior people usually want when they reach for this combinator. They are not asking which branch failed first. They are asking which branch succeeded soonest. The distinction matters because a first-error-wins rule would make mirrored or fallback requests much less useful.
The all-fail case uses the same trace shape, but the final decision changes. Suppose the cache says miss and the primary says down. There is no fulfillment left to wait for, so the combinator eventually rejects. The rejection is not a random selection from the two errors. It is a structured `AggregateError` that says the whole search space failed and preserves both reasons inside `errors`.
SourcesMDN: Promise.any (opens a new tab)MDN: AggregateError (opens a new tab)
The failure matrix explains why the API waits
A single error would be misleading here. The whole point of `Promise.any()` is that the input set is a search space for one usable result. If every branch fails, the caller usually wants to know that the entire search space failed, not just which branch happened to reject first.
`AggregateError` is the right shape because it preserves the per-branch reasons while still giving the caller one rejection to handle. That matters when the branches fail for different reasons: one request times out, another gets a 404, and a third is rejected by validation. The aggregate keeps those causes separate enough to inspect, but the API still has a single failure signal.
That separation also keeps the article honest about where `Promise.any()` stops helping. Once you have the aggregate, your code still has to decide whether to retry, fall back, display an error, or open a different route. The language gives you the batch outcome. It does not infer the business consequence.
| Situation | Result | Reason |
|---|---|---|
| One branch fulfills before the others settle | Fulfills with that value | A single success is enough to satisfy the contract |
| One branch rejects quickly, but another later fulfills | Fulfills with the later success | A rejection does not end the search while fulfillment is still possible |
| Every branch rejects | Rejects with `AggregateError` | The batch has no success left, so the error must represent all failures |
SourcesMDN: Promise.any (opens a new tab)ECMAScript specification: Promise.any (opens a new tab)
Comparison in context keeps the combinators separate
`Promise.race()` and `Promise.allSettled()` are useful contrast cases because they show how the same set of promises can answer different questions. If you use `Promise.race()`, the earliest settlement wins, even if that settlement is a rejection. That makes it a timing primitive. If you use `Promise.allSettled()`, nothing wins early because the point is to collect everything. That makes it a reporting primitive.
`Promise.any()` sits between those two ideas. It is neither first-settlement-wins nor collect-every-outcome. It is first-success-wins, and only if no success survives do we expose the full failure story. That narrow rule is why the article can be precise about `AggregateError` without drifting into a generic promise tutorial.
- Use `Promise.any()` when one success is enough.
- Use `Promise.race()` when first settlement matters more than success.
- Use `Promise.allSettled()` when you need the full outcome list.
SourcesMDN: Promise.any (opens a new tab)MDN: Promise.allSettled (opens a new tab)
The limits are part of the useful answer
There are two important boundaries to keep in view. First, `Promise.any()` does not cancel the losing promises for you. If the other branches have side effects or consume resources, those operations may continue unless your code adds explicit cancellation or cleanup.
Second, the API does not normalize the reasons inside `AggregateError`. Each branch still produces the error object it rejected with, which means your recovery logic still needs to understand the meaning of each failure class. A timeout may deserve a retry, a validation failure may need a code change, and a denied request may need a different credential or endpoint.
Those limits are the reason the article does not promise a full retry system. It is a semantics article, not an operational resilience guide. The useful result is smaller: a reader can now tell whether the combinator is behaving correctly or whether the application chose the wrong combinator for the job.
SourcesMDN: Promise.any (opens a new tab)MDN: AggregateError (opens a new tab)


