JavaScript developer reviewing Promise.any results and a separate rejection ledger to identify failed asynchronous tasks.

Promise.any Resolved. Which Rejections Were Still Lost?

Wed, Nov 11, 2026

Promise.any() answers one narrow question: did at least one input fulfill, and if so, which fulfillment arrived first? If none fulfills, it rejects with an AggregateError whose errors array contains the rejection reasons in input order, not rejection-completion order. The ECMAScript 2026 algorithm explicitly attaches a rejection handler to every input, stores each reason at that input's index, and resolves the result promise as soon as a fulfillment wins. MDN's Promise.any() reference makes the operational consequence explicit: a successful Promise.any() does not cancel the other operations; later input rejections are handled by the combinator and therefore are not, by themselves, an application-level failure ledger.

That creates the diagnostic trap this playbook targets. A top-level result can be fulfilled while one or more participating tasks have rejected before or after the winner. If release acceptance requires accounting for every task, the successful return from Promise.any() is insufficient evidence.

The controlled response is not to change Promise.any() semantics. It is to keep a separately maintained per-task rejection ledger, freeze an independent expected-case manifest before execution, retain a deliberately misleading negative control, reconcile task identities rather than infer completeness from the aggregate result, and make an explicit ACCEPT / REPAIR / RERUN / HOLD decision.

No fixture execution is represented here as observed. All test outcomes below are EXPECTED / NOT EXECUTED. The pinned runtime is Node.js 24.22.2 LTS, released October 28, 2026; official release-file SHA-256 hashes are supplied, while the hash of the locally extracted node executable remains unspecified until an operator actually installs and hashes it.

Define the admission and release contract

The admission contract begins by separating two legitimate questions.

The ECMAScript contract for Promise.any(iterable) is satisfied when the returned promise fulfills with the first input fulfillment, or rejects after all inputs reject. TC39's ECMAScript 2026 text states exactly that. The application accounting contract in this playbook is stricter: every task identity that rejects must appear exactly once in a separately maintained rejection ledger, even when Promise.any() has already fulfilled.

Those predicates must not be collapsed:

Promise.any semantic result is correct
AND
per-task failure accounting reconciles
AND
the expected case set was independently declared
AND
the evidence belongs to the pinned fixture/runtime

A release may satisfy the first line and fail the second. That is the core false-positive success condition.

The per-task ledger must therefore be owned outside AggregateError. AggregateError is available only on the all-rejected path. When one task fulfills, no aggregate rejection is delivered to the caller, even though other tasks may already have rejected or may reject later. The specification's PerformPromiseAny algorithm installs a rejection callback for every input and records reasons internally, but the internal errors list is surfaced only if the remaining-input counter reaches zero without a fulfillment.

The release gate for this fixture is:

  • ACCEPT only if the pinned source and runtime identities match, every manifest case exists exactly once, every expected rejection identity reconciles with the independent ledger, all-reject cases match expected AggregateError.errors ordering, negative controls fail in the intended discriminating way, and no unresolved discrepancy remains.

  • REPAIR when runtime semantics match the documented contract but application instrumentation loses, duplicates, rewrites, or misattributes rejection evidence.

  • RERUN when the contract is sound but evidence is absent, the fixture was not executed, hashes changed, or a collection failure makes results incomplete.

  • HOLD when task ownership, identity, cancellation consequences, or a standards/runtime contradiction cannot be reconciled safely.

This evidence-first framing is analogous, not technically equivalent, to Refonte's SQLite WAL backup committed rows playbook: that article rejects “the artifact opens” as proof that every required committed row survived. Here, “Promise.any() fulfilled” is likewise not proof that every rejected participant was accounted for. The SQLite article concerns database recovery, not JavaScript promise semantics, so its fixture and acceptance evidence are not reused.

Independent manifest, Promise.any result and per-task rejection ledger feed reconciliation and an ACCEPT, REPAIR, RERUN or HOLD decision.

Pin the baseline and source versions

The specification baseline is the versioned ECMAScript 2026 Language Specification, not a moving recollection of how promises “usually” behave. Its Promise.any section says the result fulfills on the first fulfillment and rejects with an AggregateError only when every supplied promise rejects. Its PerformPromiseAny steps allocate one indexed slot per input rejection, then create the aggregate only when the outstanding count falls to zero.

The accompanying AggregateError constructor specification defines errors as a writable, configurable, non-enumerable array created from the supplied iterable. MDN's dedicated AggregateError.errors reference similarly documents that constructor-created values preserve the order of the source iterable. For Promise.any, the more specific ordering rule follows from PerformPromiseAny: each rejection is stored at its input index, irrespective of settlement sequence.

MDN's Promise.any() page, last modified October 26, 2026, corroborates three operational details used by the manifest: an empty iterable rejects; non-promise inputs are resolved as promise values; and AggregateError.errors is in input order regardless of completion order. It also states that Promise.any() does not cancel remaining work after a fulfillment. MDN's AggregateError page was likewise last modified October 26, 2026.

The runtime fixture is pinned to Node.js v24.22.2 "Krypton" LTS, released October 28, 2026. The official release page lists V8 13.6.233.17 and publishes signed SHA-256 checksums for downloadable artifacts.

One current-date limitation matters. On November 11, 2026, the Node.js project announced security releases for the 26.x, 24.x, and 22.x lines on or shortly after that date. That does not alter the ECMAScript semantics under test, but it means v24.22.2 should be treated here as a reproducible isolated laboratory baseline, not as a recommendation to deploy that version to an exposed service.

Three related playbooks examine different contracts. AbortSignal.any Fired. Which Cancellation Reason Reached Fetch? examines JavaScript asynchronous composition, provenance, an independent manifest, and evidence identities; its subject is abort-source selection and fetch rejection, not Promise.any rejection accounting. Keep an Older Fetch Response From Replacing the Latest Search addresses asynchronous completion ordering and ownership, but its acceptance object is UI mutation authority rather than promise rejection completeness. A GitHub Actions Job Passed. Did Every Matrix Combination Run? rejects an aggregate green status without an independent denominator of expected cases. This playbook focuses on which Promise.any participants rejected when the combinator itself ultimately fulfilled.

Pin the runtime and source hashes

Use the official Linux x64 binary archive and source archive from the Node.js 24.22.2 release.

Node release:              v24.22.2
Release date:              2026-10-28
Linux x64 .tar.xz SHA-256:
48cc0e21e048790e54c0cf5749c2725005313d652651c40b7a21feb9697aaf75

Source .tar.gz SHA-256:
b2f5443a34a38d863e87c0f5f80c03630fc16917dfe16f26c1cec44c0b542959

Source .tar.xz SHA-256:
8fe2ec1723bb9f8d0b683907c82708debbeda64af21cb2aa214c4241eed0a061

Extracted node executable SHA-256:
UNSPECIFIED UNTIL LOCALLY INSTALLED AND HASHED

Those release-file hashes are published by Node.js.

The exact assertion is: the downloaded archive hash must equal the official release hash, node --version must return v24.22.2, and the operator must record the extracted executable hash before the case run.

Expected outcome: EXPECTED / NOT EXECUTED: all three provenance checks match the recorded baseline.

Rejection condition: any archive mismatch, runtime-version mismatch, or missing executable hash makes an attempted evidence run RERUN; an unexplained provenance mismatch is HOLD.

Build an isolated fixture with synthetic inputs

Use no network, framework, production service, credentials, package dependencies, clocks, or external queues. The fixture needs only controlled promises, literal synthetic error values, AbortController, filesystem output, and Node's built-in assertion facilities.

That isolation is deliberate. Timer-based tests can accidentally test scheduler timing rather than promise semantics. Instead, each task exposes explicit fulfill() and reject() controls. The runner creates all promises first, calls Promise.any(), then settles participants in an authored sequence. “Slow” therefore means “settled after the winner,” not “sleep for an arbitrary number of milliseconds.”

Create a disposable directory:

set -euo pipefail

rm -rf promise-any-audit
mkdir -p promise-any-audit/evidence
cd promise-any-audit

node --version
node -p 'JSON.stringify(process.versions, null, 2)' \
  > evidence/process-versions.json

sha256sum "$(command -v node)" \
  > evidence/node-executable.sha256

The fixture structure is intentionally small:

promise-any-audit/
├── manifest.json
├── fixture.mjs
└── evidence/
    ├── process-versions.json
    ├── node-executable.sha256
    ├── evidence.json
    ├── stdout.log
    └── source.sha256

A controlled task records its identity at the rejection boundary and then rethrows the same rejection value into the promise chain:

function trackedTask(taskId, rejectionLedger) {
  let resolveRaw;
  let rejectRaw;

  const raw = new Promise((resolve, reject) => {
    resolveRaw = resolve;
    rejectRaw = reject;
  });

  const promise = raw.catch((reason) => {
    rejectionLedger.push({
      seq: rejectionLedger.length + 1,
      taskId,
      reason,
    });
    throw reason;
  });

  return {
    taskId,
    promise,
    fulfill: resolveRaw,
    reject: rejectRaw,
  };
}

The rethrow matters. Swallowing the rejection would convert the tracked task into a fulfillment and would change what Promise.any() is testing. Reconstructing a new error would also destroy strict reason identity.

This fixture's isolation philosophy overlaps with the SQLite BUSY_SNAPSHOT transaction retry article, which uses deterministic controlled interleavings instead of hoping a concurrency race happens. That article validates transaction retry boundaries; the present fixture uses the same falsification discipline but no SQLite code or transaction evidence.

Author the independent expected-case manifest

The oracle must be written before execution and kept outside the runner. Do not generate manifest.json by running the fixture and serializing whatever happened. That would allow the implementation to define its own correctness.

The minimum independent manifest is:

Case

Scenario

Promise.any expected result

Independent rejection evidence

Status

first-fulfillment

reject A, fulfill B, later reject C

fulfill winner-B

A and C exactly once

EXPECTED / NOT EXECUTED

all-reject

B rejects, then C, then A

AggregateError

ledger B,C,A; aggregate A,B,C

EXPECTED / NOT EXECUTED

empty-iterable

no inputs

AggregateError

no task rows; errors.length === 0

EXPECTED / NOT EXECUTED

rejection-order

C,B,A settle

AggregateError

ledger C,B,A; aggregate A,B,C

EXPECTED / NOT EXECUTED

slow-reject-after-fulfillment

winner fulfills, D rejects later

fulfill winner

D still appears once

EXPECTED / NOT EXECUTED

nonpromise-value

literal value among promises

fulfill literal

rejected promise tasks still reconcile

EXPECTED / NOT EXECUTED

duplicate-error-values

A and B reject same Error object

AggregateError

two task IDs despite identical reason identity

EXPECTED / NOT EXECUTED

abort-unused-work

winner fulfills; owned pending work explicitly aborted

winner remains fulfilled

aborted task IDs each recorded once

EXPECTED / NOT EXECUTED

A literal manifest.json can encode only stable externally judged facts:

{
  "schema": 1,
  "runtime": "v24.22.2",
  "cases": {
    "first-fulfillment": {
      "any": {"type":"fulfilled","value":"winner-B"},
      "rejectionTaskIds":["A","C"]
    },
    "all-reject": {
      "any":{"type":"rejected","name":"AggregateError"},
      "ledgerOrder":["B","C","A"],
      "aggregateReasonTags":["A","B","C"]
    },
    "empty-iterable": {
      "any":{"type":"rejected","name":"AggregateError"},
      "aggregateLength":0
    },
    "rejection-order": {
      "ledgerOrder":["C","B","A"],
      "aggregateReasonTags":["A","B","C"]
    },
    "slow-reject-after-fulfillment": {
      "any":{"type":"fulfilled","value":"winner"},
      "rejectionTaskIds":["D"]
    },
    "nonpromise-value": {
      "any":{"type":"fulfilled","value":"literal"},
      "rejectionTaskIds":["A","C"]
    },
    "duplicate-error-values": {
      "rejectionTaskIds":["A","B"],
      "aggregateLength":2,
      "sameReasonIdentity":true
    },
    "abort-unused-work": {
      "any":{"type":"fulfilled","value":"winner"},
      "rejectionTaskIds":["A","B"]
    }
  }
}

Freeze the oracle before executing

Freeze and hash the oracle separately:

chmod a-w manifest.json
sha256sum manifest.json > evidence/manifest.sha256
cat evidence/manifest.sha256

Exact assertion: the SHA-256 recorded immediately before execution must equal the hash reconciled with the evidence bundle afterward.

Expected outcome: EXPECTED / NOT EXECUTED: manifest bytes do not change during the run.

Rejection condition: if the manifest was edited after seeing runtime results, the test is not independent. Classify that evidence as RERUN, not ACCEPT.

Reproduce the misleading success condition

The negative control intentionally asks too little:

async function flawedSuccessOnly(tasks) {
  return Promise.any(tasks.map((task) => task.promise));
}

Given three tasks, an operator can settle them as:

const ledger = [];
const a = trackedTask("A", ledger);
const b = trackedTask("B", ledger);
const c = trackedTask("C", ledger);

const result = flawedSuccessOnly([a, b, c]);

a.reject(Object.assign(new Error("A failed"), { tag: "A" }));
b.fulfill("winner-B");

console.log(await result);

// Later:
c.reject(Object.assign(new Error("C failed"), { tag: "C" }));
await Promise.allSettled([a.promise, b.promise, c.promise]);

The top-level result is expected to be winner-B. That is correct Promise.any() behavior, not a runtime defect. TC39 says the first fulfillment resolves the result capability, while rejection handlers remain attached to the participating promises.

The false positive appears when the application equates that fulfilled aggregate result with “no relevant failures occurred.” The successful value contains no list of earlier or later rejected tasks. The all-rejected AggregateError path never happens, so its errors array cannot serve as a universal accounting record.

MDN explicitly notes that subsequent rejections are ignored by the already-settled Promise.any() result and do not create unhandledrejection events merely because they occur after another input fulfilled; the combinator has already attached handlers. It also states that fulfillment does not cancel remaining operations.

That is precisely why checking for unhandledRejection is not a substitute for a business or release ledger. “No unhandled rejection” means the promise machinery has a handler path; it does not mean no participating task rejected.

For this article the negative-control outcome remains EXPECTED / NOT EXECUTED. No console output above is represented as an observed run.

Run the controlled positive comparison

The repaired comparison retains the same Promise.any() semantics and adds independent per-task accounting.

A compact runnable driver is:

// fixture.mjs
import fs from "node:fs/promises";
import assert from "node:assert/strict";

const manifest =
  JSON.parse(await fs.readFile(new URL("./manifest.json", import.meta.url)));

const evidence = [];

function tag(tag) {
  return Object.assign(new Error${tag} failed), { tag });
}

function trackedTask(taskId, rejectionLedger) {
  let fulfill;
  let reject;

  const raw = new Promise((resolve, rejectFn) => {
    fulfill = resolve;
    reject = rejectFn;
  });

  const promise = raw.catch((reason) => {
    rejectionLedger.push({
      seq: rejectionLedger.length + 1,
      taskId,
      reasonTag: reason?.tag ?? reason?.name ?? String(reason),
      reason
    });
    throw reason;
  });

  return { taskId, promise, fulfill, reject };
}

async function captureAny(inputs) {
  try {
    return { type: "fulfilled", value: await Promise.any(inputs) };
  } catch (error) {
    return {
      type: "rejected",
      name: error?.name,
      error,
      errors: error?.errors
    };
  }
}

async function runCase(caseId, body) {
  const rejectionLedger = [];
  const result = await body(rejectionLedger);

  const row = {
    caseId,
    any: {
      type: result.any.type,
      value: result.any.value,
      name: result.any.name
    },
    aggregateReasonTags:
      result.any.errors?.map((x) => x?.tag ?? x?.name ?? String(x)),
    rejectionLedger:
      rejectionLedger.map(({seq, taskId, reasonTag}) =>
        ({seq, taskId, reasonTag}))
  };

  evidence.push(row);
  return { result, rejectionLedger, row };
}

await runCase("first-fulfillment", async (ledger) => {
  const a = trackedTask("A", ledger);
  const b = trackedTask("B", ledger);
  const c = trackedTask("C", ledger);

  const anyP = captureAny([a.promise, b.promise, c.promise]);
  a.reject(tag("A"));
  b.fulfill("winner-B");

  const any = await anyP;

  c.reject(tag("C"));
  await Promise.allSettled([a.promise, b.promise, c.promise]);

  return { any };
});

await runCase("all-reject", async (ledger) => {
  const a = trackedTask("A", ledger);
  const b = trackedTask("B", ledger);
  const c = trackedTask("C", ledger);

  const anyP = captureAny([a.promise, b.promise, c.promise]);
  b.reject(tag("B"));
  c.reject(tag("C"));
  a.reject(tag("A"));

  const any = await anyP;
  await Promise.allSettled([a.promise, b.promise, c.promise]);
  return { any };
});

await runCase("empty-iterable", async () => ({
  any: await captureAny([])
}));

await runCase("rejection-order", async (ledger) => {
  const a = trackedTask("A", ledger);
  const b = trackedTask("B", ledger);
  const c = trackedTask("C", ledger);

  const anyP = captureAny([a.promise, b.promise, c.promise]);
  c.reject(tag("C"));
  b.reject(tag("B"));
  a.reject(tag("A"));

  return { any: await anyP };
});

await fs.mkdir(new URL("./evidence/", import.meta.url), { recursive: true });
await fs.writeFile(
  new URL("./evidence/evidence.json", import.meta.url),
  JSON.stringify({ manifestRuntime: manifest.runtime, evidence }, null, 2)
);

assert.equal(manifest.runtime, "v24.22.2");
console.log(JSON.stringify(evidence, null, 2));

Add the remaining four cases using the same primitives rather than changing the oracle-generation model.

Run with:

set -euo pipefail

test "$(node --version)" = "v24.22.2"

sha256sum manifest.json fixture.mjs \
  | tee evidence/source.sha256

node --trace-warnings fixture.mjs \
  > evidence/stdout.log \
  2> evidence/stderr.log

printf '%s\n' "$?" > evidence/exit-status.txt

A zero process exit remains necessary but not sufficient.

The methodological comparison with GitHub Actions matrix coverage is direct: that article requires an independently authored expected denominator because a green workflow can coexist with absent or cancelled matrix children. Here a fulfilled aggregate promise can coexist with rejected child tasks. The domains and fixtures are different; the evidence pattern is the useful overlap.

Retain the negative-control evidence

Do not delete the success-only negative control after repairing the ledger.

Exact assertion: the negative control must still demonstrate that Promise.any() can fulfill without returning per-task rejection accounting, while the instrumented path must retain those rejection identities separately.

Expected outcome: EXPECTED / NOT EXECUTED.

Rejection condition: if the “negative” control starts producing the same complete ledger by accidental reuse of repair instrumentation, it no longer discriminates between defective and repaired accounting. The fixture needs REPAIR before rerun.

Probe failures and boundary conditions

The boundary cases exist to prevent an implementation-specific happy path from becoming the contract.

All reject. TC39's algorithm stores each rejection at the corresponding input index, then constructs the aggregate when the final outstanding input rejects. If settlement order is B, C, A for an input list A, B, C, the ledger should show B, C, A while AggregateError.errors should correspond to A, B, C.

Empty iterable. Promise.any([]) rejects because there is no possible fulfilling member. MDN documents the empty-input rejection and the ECMAScript algorithm's zero-remaining-elements path creates an AggregateError with an empty errors array.

Non-promise value. Promise.any() applies the constructor's promise-resolution operation to each iterable element, so a literal value is eligible to fulfill the aggregate. MDN describes non-thenables as resolved as-is. The ledger still has to account for any promise participants that reject.

Duplicate error values. Two distinct tasks may reject with the same JavaScript error object. AggregateError.errors can therefore contain repeated references. A ledger keyed only by error identity cannot distinguish those tasks; task identity must be a separate field.

Late rejection after fulfillment. This is the highest-value false-positive probe. Resolve the winner, await the aggregate fulfillment, reject another task afterward, and then deliberately drain all participants with Promise.allSettled() so evidence collection does not finish early.

Explicit abort. Promise.any() itself provides no cancellation contract. MDN explicitly warns that remaining operations are not cancelled by fulfillment. If the application chooses to abort unused work, that is a separate application policy and must be tested independently. The closest Refonte article, AbortSignal.any Fired. Which Cancellation Reason Reached Fetch?, likewise treats cancellation provenance as a distinct contract rather than assuming an aggregate state proves which source caused it.

An abort test should therefore prove both facts: the aggregate winner remains the original fulfillment, and each owned pending task that responds to the explicit abort produces the expected rejection ledger row.

Capture a per-case evidence ledger

The runtime evidence should be machine-readable and case-addressable.

For each case, retain:

Field

Purpose

caseId

joins evidence to the independent manifest

runtime version

proves execution environment

fixture source hash

binds behavior to runner bytes

manifest hash

proves oracle bytes

aggregate outcome

fulfilled or rejected

fulfillment value

identifies winner where declared

aggregate error type

validates all-reject path

aggregate rejection tags

validates indexed input ordering

ledger sequence

records rejection observation order

taskId

independent task identity

reason tag/type

diagnostic payload

duplicate count

exposes double recording

missing expected IDs

exposes lost accounting

unexpected IDs

exposes contamination

final disposition

ACCEPT / REPAIR / RERUN / HOLD

Before any run, the article-level expected ledger is therefore:

Case

Expected ledger state

Observed state

first fulfillment

A,C, once each

NOT EXECUTED

all reject

B,C,A; aggregate A,B,C

NOT EXECUTED

empty iterable

zero task rows

NOT EXECUTED

rejection order

C,B,A; aggregate A,B,C

NOT EXECUTED

slow reject after fulfillment

late task exactly once

NOT EXECUTED

non-promise value

rejecting promise participants reconcile

NOT EXECUTED

duplicate error values

two task rows despite same reason object

NOT EXECUTED

abort unused work

every explicitly aborted owned task once

NOT EXECUTED

No placeholder should be upgraded from NOT EXECUTED because the specification predicts the result. Documented behavior is an oracle; it is not an observation of this particular Node binary, source bundle, or fixture invocation.

Reject missing and duplicate records

For each case compute identity multisets, not just counts.

function multiplicities(ids) {
  const out = new Map();
  for (const id of ids) out.set(id, (out.get(id) ?? 0) + 1);
  return out;
}

Exact assertion: every manifest-required rejection task has multiplicity one, every observed ledger task is expected for that case, and no case ID appears twice in the selected run bundle.

Expected outcome: EXPECTED / NOT EXECUTED: missing, duplicate, and unexpected sets are all empty.

Rejection condition: a missing expected task is REPAIR if instrumentation is known to have dropped it; it is RERUN if collection terminated early; an unexplained identity collision is HOLD.

Reconcile identities and final state

Reconciliation should happen after the case's owned work reaches a declared terminal state. That may be later than await Promise.any(...).

For first-fulfillment cases the sequence is:

create participants
        ↓
start Promise.any
        ↓
settle chosen rejection(s)
        ↓
settle winner
        ↓
capture Promise.any fulfillment
        ↓
settle/abort remaining owned work
        ↓
Promise.allSettled(all participant promises)
        ↓
reconcile independent rejection ledger

This avoids a subtle evidence truncation error: ending collection at the first fulfillment proves exactly the same narrow thing that Promise.any() already proved.

For all-reject cases, perform two reconciliations. First, compare ledger task identities against the manifest's expected rejected tasks. Second, compare AggregateError.errors against the input-index oracle. These answer different questions. The ledger records task provenance and chronological observation; the aggregate preserves the reasons by iterable position.

Never reconcile by error.message. Error text is not a stable identity. MDN even documents differing engine messages for the built-in all-rejected Promise.any() condition, including V8, Firefox, and Safari variants. The fixture should assert instanceof AggregateError, error.name, errors.length, reason identity or synthetic tags, and task IDs, rather than a human-readable engine message.

The duplicate-error-values case exists specifically to defeat an error-object-keyed accounting design. If tasks A and B reject the same Error instance, a Set of reasons has cardinality one while the task ledger must have cardinality two. Reconciliation must therefore key on the task identity declared before execution.

The analogy to Probability calibrator release validation is limited but useful: that playbook refuses to approve a superficially good metric unless the candidate artifact is paired with the intended base-model identity. Here, a correct aggregate outcome is accepted only when it is paired with the intended participant identities and independent rejection evidence. The model-calibration workflow itself is not reused.

Separate documentation from observation

A rigorous report uses four labels consistently.

DOCUMENTED means the result follows from the governing reference. For example: Promise.any() fulfills on first fulfillment; all-reject produces AggregateError; rejections are stored by input index; empty input rejects.

HYPOTHESIS means an application-specific proposition to test. Example: “our wrapper records every rejected participant exactly once without changing the rejection reason.”

EXPECTED / NOT EXECUTED means the fixture and governing documentation imply a result, but the pinned fixture has not been run for this article.

OBSERVED must be reserved for bytes collected from an actual execution. This article has no OBSERVED case results.

There is no substantive TC39/MDN disagreement on the behavior under test. There are, however, wording differences that should not be mistaken for contradictions. MDN calls Promise.any() “short-circuiting” after a fulfillment, but the same page explicitly says remaining operations are not cancelled. “Short-circuit” describes settlement of the aggregate result, not termination of underlying work.

Likewise, MDN describes the empty iterable's returned promise as already rejected. That is compatible with promise reaction callbacks still running through the normal asynchronous job machinery; do not turn the documentation wording into a claim that a .catch() callback executes synchronously.

Another versioning distinction is visible as of November 2026: MDN's living page links onward to a newer living ECMAScript specification, while this playbook deliberately freezes the ECMAScript 2026 edition for reproducibility. That is a source-version choice, not evidence that the relevant semantics differ.

Explain the repair boundary

Exact assertion: the repair may add accounting around each task, but it must not convert a rejection into fulfillment, replace the rejection value, change task ordering, or claim that Promise.any() itself performs cancellation.

Expected outcome: EXPECTED / NOT EXECUTED: with ledger instrumentation enabled, the aggregate result remains identical to the uninstrumented control for every semantic case.

Rejection condition: if the repair changes which participant wins, changes all-reject reason identity, or swallows a rejection, classify REPAIR and do not use its ledger as evidence.

Implement the smallest safe repair

The smallest repair is an identity-preserving rejection tap:

function withRejectionLedger(taskId, promise, ledger) {
  return Promise.resolve(promise).catch((reason) => {
    ledger.push({
      taskId,
      seq: ledger.length + 1,
      reason
    });
    throw reason;
  });
}

Then compose the wrapped promises:

const ledger = [];

const participants = [
  withRejectionLedger("region-a", taskA(), ledger),
  withRejectionLedger("region-b", taskB(), ledger),
  withRejectionLedger("region-c", taskC(), ledger)
];

const winner = await Promise.any(participants);

This deliberately does not manufacture an AggregateError after a successful winner. Doing so would conflate two APIs: the language-level result of Promise.any() and the application's post-run audit. Instead, record the winner separately and reconcile the ledger once the lifecycle policy says remaining work has reached an evidence boundary.

The wrapper also must not use:

.catch((reason) => {
  ledger.push(reason);
  return undefined;        // WRONG: converts rejection to fulfillment
});

Nor should it do:

.catch((reason) => {
  ledger.push(reason);
  throw new Error(reason.message); // WRONG: destroys identity
});

The repaired task must reject with the original reason so that Promise.any() sees the same semantics it would have seen without instrumentation.

When task code owns a richer task identifier, record that identifier before starting work. Do not derive it afterward from a mutable message, URL string, stack trace, or error type.

For a successful aggregate, completion policy must also say what to do with unused work. Options may include allowing owned work to finish, explicitly aborting APIs that support cancellation, or transferring ownership to another lifecycle. Promise.any() does not choose among those application policies. MDN explicitly states that remaining operations are not automatically cancelled.

Test rollback, recovery and rerun safety

Rollback begins by separating the semantic change from the observability change.

A rejection-ledger wrapper that rethrows the same value should be independently removable without redesigning the Promise.any() selection contract. If a release of the ledger code causes unacceptable overhead or malformed evidence, the safe recovery is to revert the instrumentation change and return to the previous selection behavior, rather than mutate Promise.any() semantics merely to make the ledger easier.

Rerun safety requires a fresh evidence directory or run ID. Never append a second execution to the first run's ledger and then deduplicate afterward; that can hide actual duplicate instrumentation. Capture fixture hash, manifest hash, runtime hash, invocation, exit status, stdout/stderr, and evidence JSON per attempt.

For cases with only synthetic controlled promises, rerun has no external side effects. That property does not automatically transfer to application code. If the real tasks create orders, charge cards, send mail, publish messages, or mutate remote state, replay ownership has to be defined by the responsible subsystem before automated rerun is approved.

Cancellation requires the same caution. Aborting unused work is safe only when the task API participates in cancellation and the application owns the decision to stop it. The fact that the aggregate already fulfilled does not prove that every losing operation is disposable.

A practical recovery record should contain:

{
  "runId": "RECORD_AT_EXECUTION",
  "manifestSha256": "RECORD_AT_EXECUTION",
  "fixtureSha256": "RECORD_AT_EXECUTION",
  "nodeExecutableSha256": "RECORD_AT_EXECUTION",
  "decision": "RERUN",
  "reason": "fixture has not been executed",
  "supersedesRunId": null
}

Refonte's Pytest strict XPASS release gate makes a related distinction between a process-level success signal and the release meaning of individual test outcomes. Its subject is pytest exception policy rather than promises, but the anti-pattern is analogous: a green command should not silently redefine a more specific acceptance contract.

Assign recovery ownership

Exact assertion: every rerun or abort decision must identify who owns repeating or stopping the underlying task category.

Expected outcome: EXPECTED / NOT EXECUTED: the synthetic fixture owner can rerun all eight cases because they have no external effects.

Rejection condition: if real task side effects, idempotency, or cancellation ownership are unknown, the correct decision is HOLD, not an automatic rerun.

Apply the ACCEPT / REPAIR / RERUN / HOLD matrix

The decision matrix prevents the aggregate promise's result from becoming the release decision by accident.

Evidence state

Decision

Reason

All manifest cases executed under pinned runtime; source/runtime hashes match; rejection identities reconcile exactly; negative control discriminates; no unexplained warnings

ACCEPT

Contract and evidence agree

Promise.any() semantics match TC39, but ledger misses, duplicates, transforms, or misattributes rejected tasks

REPAIR

Application accounting is defective

Fixture or manifest changed unintentionally, execution stopped early, evidence file is absent, or tests were not executed

RERUN

Contract may be valid but evidence is incomplete

Runtime contradicts governing contract without explanation, task ownership is ambiguous, abort could cause unsafe side effects, or identities cannot be reconciled

HOLD

Automated approval or retry is unsafe

For the artifact as written here, the final release decision is:

RERUN
Status: EXPECTED / NOT EXECUTED

Reason:
The specification and fixture define a testable contract, but no
execution evidence has been collected from the pinned Node.js v24.22.2
runtime. Therefore ACCEPT would be invented.

That is intentionally stricter than saying the examples are “obviously correct.” TC39 and MDN establish the expected language behavior. They do not establish that the supplied fixture bytes were executed, that a particular downloaded Node binary matched its release archive, that local instrumentation retained every task identity, or that the evidence files were complete.

The intended post-execution acceptance command sequence is:

set -euo pipefail

test "$(node --version)" = "v24.22.2"

sha256sum "$(command -v node)" \
  > evidence/node-executable.sha256

sha256sum manifest.json fixture.mjs \
  > evidence/source.sha256

node --trace-warnings fixture.mjs \
  > evidence/stdout.log \
  2> evidence/stderr.log

status=$?
printf '%s\n' "$status" > evidence/exit-status.txt
test "$status" -eq 0

sha256sum evidence/* > evidence/evidence-files.sha256

A reviewer then reconciles rather than merely reading exit-status.txt.

The acceptance logic can be summarized as:

Acceptance flow checks source and runtime identity, complete manifest cases, reconciled ledger identities, aggregate semantics and discriminating negative controls.

Develop related skills with Refonte Learning

The Refonte Learning Frontend Development program is described as a four-month program with a stated dedication of 10–12 hours per week and explicitly lists JavaScript ES6+, React.js, Redux, APIs/AJAX, Git, responsive web development, accessibility, and performance optimization among its competencies. It also states a basic programming-understanding requirement, lists familiarity with HTML/CSS as a plus, and separately publishes an admission prerequisite of working toward a bachelor's or higher-level degree.

The page does not specifically claim specialized instruction in Promise.any(), AggregateError rejection-ledger reconciliation, this fixture, or this release-acceptance method. The relevant connection is therefore broad JavaScript and frontend engineering foundations, not a claim that the program teaches this exact playbook. Engineers who want that broader structured context can review the Refonte Learning Frontend Development program.