Skip to content
R.

Why does `Promise.allSettled` return results in input order?

`Promise.allSettled` keeps one result slot per input promise, so completion order and result order can differ. See the guarantee, the reason it exists, and a regression test that catches the wrong assumption.

About 10 min readComments

It is easy to read `Promise.allSettled` as if it returns whatever finishes first and then appends the rest. That is not the contract. The method preserves the order of the input iterable, even when the fastest promise settles in the middle of the batch.

That distinction matters any time you label results by their original position, pair responses with the request list, or summarise a batch after every task has finished. The article shows the guarantee with a deterministic Node example and a regression test.

What the API actually guarantees

`Promise.allSettled` returns a promise that fulfills only after every input promise has settled. The fulfillment value is an array of result objects, one per input item, and the array is ordered the same way as the input iterable, not the way the promises happened to finish. MDN states that explicitly, and the ECMAScript spec implements it by assigning each input an index before any callback runs.

That is why the returned array is useful for batch reporting. If your input list is `[alpha, beta, gamma]`, result slot 0 always refers to `alpha`, even if `beta` settles first and `gamma` settles second. The method keeps the original alignment so downstream code can compare the response with the request without additional bookkeeping.

The important detail is that each input gets an index before its settlement handler records the outcome. The promise itself may already be fulfilled or rejected when it is supplied. Input alignment still lets a consumer match a result to a row, form field, or retry descriptor without reconstructing the timing history. Keep those descriptors available: the result entry carries an outcome, not your application's task ID.

Completion order and result order describe different things.
QuestionWhat it answersWhat `Promise.allSettled` uses
Completion orderWhich async task finished first?Not the return order
Result orderWhich input position does this slot represent?The returned array keeps that order
StatusDid the task fulfill or reject?Each entry records one status snapshot

SourcesMDN: Promise.allSettled() (opens a new tab)ECMAScript spec: Promise.allSettled (opens a new tab)

A worked example with out-of-order completion

The introductory example uses three labeled tasks with different delays. Under normal execution, the second finishes first, followed by the third and then the first. Treat this as a demonstration, not a timing guarantee. The regression tests below avoid clock assumptions by explicitly settling promises in a selected sequence. The useful distinction is between the observed completion log and the returned input-aligned array.

That is the part people often miss when they port code from a streaming mental model. `allSettled` is not a timeline; it is a stable summary. The stable summary is what makes it safe to zip the returned array against the original input list after the batch is done.

If you need to emit progress while the batch runs, you can still log or stream events from each task, but that progress channel is separate from the final settlement summary. Mixing the two is what creates confusion: the event log tells you when something happened, while the settled array tells you what belonged to each original slot.

Controlled example / javascript
const batch = [
  { name: 'alpha', delay: 40 },
  { name: 'beta', delay: 5 },
  { name: 'gamma', delay: 20 },
];

const completion = [];
const settled = await Promise.allSettled(batch.map(async item => {
  await new Promise(resolve => setTimeout(resolve, item.delay));
  completion.push(item.name);
  return { name: item.name, delay: item.delay };
}));

console.log(completion);
console.log(settled.map(entry => entry.value?.name ?? entry.reason?.name));

SourcesMDN: Promise.allSettled() (opens a new tab)ECMAScript spec: PerformPromiseAllSettled (opens a new tab)

The bug appears when you treat completion as position

The most common mistake is to assume the first settled promise should occupy result slot 0. That assumption fails as soon as one promise is faster than the others. In the local fixture, `beta` finishes first, but it still appears in result slot 1 because it was the second input item.

That bug usually shows up when code pairs labels, IDs, or UI rows with results after the fact. If you mutate the descriptor list while the batch is pending, or match the output against completion sequence instead of the original input sequence, you can silently report a task under the wrong label. Sorting both promises and descriptors together before starting the batch is different: that establishes a new, consistent input order.

A related mistake is to assume a rejected promise should somehow vanish from the summary. It does not. Rejections still occupy their original slots, which means the right consumer pattern is to read `status` first and then inspect `value` or `reason` for that same position. Removing rejected entries from the middle of the array breaks the alignment the API is trying to preserve.

SourcesMDN: Promise.allSettled() (opens a new tab)

Lock the assumption down with a regression test

A regression test should prove both sides of the contract: the completion log is out of order, and the settled array still follows the original input positions. That is more useful than merely asserting that the method returns an array, because the array shape is not the part people usually get wrong.

The tests use Node's built-in runner and deferred promises instead of short timers. The fixture rejects beta, resolves gamma, and finally resolves alpha, yielding to promise callbacks between those actions. It checks the deliberately selected completion order and the different input order. This removes an avoidable dependency on machine load while retaining a real use of the native Promise implementation.

The rejected-promise case in the same test file protects a second boundary: the method does not throw just because one input rejects. That matters because the meaningful contract is not only that the order is stable, but that every slot becomes a status object even when the batch contains mixed outcomes.

Regression test / javascript
import assert from 'node:assert/strict';
import test from 'node:test';

function deferred() {
  let resolve, reject;
  const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
  return { promise, resolve, reject };
}

test('input slots survive out-of-order settlement', async () => {
  const tasks = [deferred(), deferred(), deferred()];
  const result = Promise.allSettled(tasks.map(task => task.promise));
  tasks[1].reject(new Error('permission denied'));
  await Promise.resolve();
  tasks[2].resolve('gamma');
  await Promise.resolve();
  tasks[0].resolve('alpha');
  const settled = await result;
  assert.deepEqual(settled.map(entry => entry.status), ['fulfilled', 'rejected', 'fulfilled']);
  assert.equal(settled[0].value, 'alpha');
  assert.equal(settled[1].reason.message, 'permission denied');
  assert.equal(settled[2].value, 'gamma');
});

SourcesNode.js test runner (opens a new tab)MDN: Promise.allSettled() (opens a new tab)

Attach labels before filtering successful rows

Consider a dashboard that refreshes three account rows. Alpha and gamma load successfully, while beta fails with a permission error. The final result array has three entries, including beta's rejected entry. If the UI first removes rejected entries and then assigns names by the remaining index, gamma's payload is displayed under beta. Nothing throws: the application simply tells the user the wrong thing. The bug is in the consumer, not in promise ordering.

Keep a snapshot of the input descriptors and attach each descriptor to its result before applying display filters. In this example, the corrected rows carry alpha, beta, and gamma as their own metadata. Filtering that labeled collection then keeps alpha and gamma. The regression suite deliberately implements the broken filter-first version and asserts its incorrect output, followed by the corrected association. This makes the failure concrete instead of relying on a warning in a comment.

The rejection reason is not a reliable task identifier. An Error normally has a name such as Error or TypeError; a rejection can also carry a string or another value. Do not try to reconstruct a business label from reason.name. Store the task ID alongside the promise before starting the batch. Likewise, avoid sorting or mutating the descriptor array while requests are pending: a stable result index is only useful if the list you match it to still represents the original input.

Keep metadata attached to each outcome / javascript
const inputs = [{ id: 'alpha' }, { id: 'beta' }, { id: 'gamma' }];
const settled = await Promise.allSettled([
  Promise.resolve('first payload'),
  Promise.reject(new Error('permission denied')),
  Promise.resolve('third payload'),
]);
const rows = settled.map((outcome, index) => ({ ...inputs[index], ...outcome }));
const successfulRows = rows.filter(row => row.status === 'fulfilled');
console.log(successfulRows.map(row => row.id)); // ['alpha', 'gamma']

SourcesMDN: Promise.allSettled() (opens a new tab)

Test the batch that never finishes and the batch with no work

A reporting screen can remain in its loading state if one input never settles. The added fixture creates one deferred promise, observes that the aggregate callback has not run after flushing microtasks, and then explicitly resolves that input to let the test finish. It proves the boundary without leaving a hanging test process. In a real request layer, define a timeout or cancellation policy for the underlying operation; the summary combinator cannot invent an outcome for work that remains pending.

An empty batch has a different boundary. There is no work to wait for, but a then callback still does not interrupt the current synchronous statement sequence. The regression records a synchronous marker followed by the callback marker and asserts that order. A UI should therefore handle its zero-row state explicitly rather than infer that a callback has already updated state simply because the request list is empty. Neither test measures browser paint timing, network latency, or how a framework batches state updates.

The fixture uses synthetic tasks, so it does not validate credentials, HTTP status handling, retry safety, or a server's response schema. A production integration needs separate assertions for those contracts. Also remember that waiting for all results is not a concurrency limit: constructing a large batch can start substantial work before the aggregate is awaited. When a service has rate limits, bound the task launcher independently and preserve the original IDs through that scheduler.

SourcesECMAScript: Promise.allSettled (opens a new tab)Node.js test runner (opens a new tab)

Where this contract stops

The ordering guarantee does not give you cancellation, deduplication, or early exit. `Promise.allSettled` still observes every input promise, so the slowest task can continue running after the fastest one has already finished. That makes it useful for complete reporting, but not for shutting work down.

It also does not turn rejection into success. A rejected promise still occupies its original slot, but the entry contains a rejection snapshot instead of a fulfillment value. If you only care about the first success, or you want to fail fast on the first rejection, another combinator is a better fit.

That limit matters in user-facing batch tools. A dashboard that should stop after the first confirmed upload, or a worker that needs to cancel the rest of a job once one branch fails, needs additional control flow around the promises. `allSettled` is the right end-state summary when the point of the batch is to collect every outcome, not to steer the work in flight.

  • Use `Promise.allSettled` when you need the full batch outcome and stable input alignment.
  • Use `Promise.all` when the aggregate should reject as soon as an input rejects; this does not cancel other work.
  • Use `Promise.any` when you only care about the first fulfillment.
  • Treat a result array as a summary, not as evidence of completion sequence.
  • If the result order matters for presentation, keep the original input list available so the summary can be read against the same positions.

SourcesMDN: Promise.allSettled() (opens a new tab)ECMAScript spec: Promise.allSettled (opens a new tab)

Share LinkedIn Email Subscribe

Discussion

Leave a comment

Comments appear after review. No email needed.

Image preview

Follow the blog

New articles in your feed. No email needed.

Use OpenRSS

Preview the feed, then choose a reader to subscribe.

Open in OpenRSS (opens a new tab)

Already have a reader?

Paste this link into your reader's Add feed option.

Get full articles in your reader, not your inbox. View XML feed