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/runtimeA 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.
|
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 HASHEDThose 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.sha256The fixture structure is intentionally small:
promise-any-audit/
├── manifest.json
├── fixture.mjs
└── evidence/
├── process-versions.json
├── node-executable.sha256
├── evidence.json
├── stdout.log
└── source.sha256A 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.sha256Exact 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.txtA 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 ledgerThis 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.sha256A reviewer then reconciles rather than merely reading exit-status.txt.
The acceptance logic can be summarized as:
|
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.


