AbortSignal.any() is easy to read as “whichever cancellation happens first wins.” That is incomplete in the case that causes the most expensive diagnostic mistakes: one or more inputs may already be aborted before composition. The DOM algorithm checks the input sequence in order and returns as soon as it encounters an already-aborted source; the composite therefore inherits that source’s reason immediately. For live sources, the first source to abort becomes the provenance. fetch() then has its own signal-following path, so a release test should not stop at combined.aborted === true; it should prove which source reason reached the combined signal and which value rejected the fetch promise. The DOM Standard specifies the composition behavior, while the Fetch Standard specifies rejection with the request signal’s abort reason.
This playbook tests that narrow chain in a disposable Node.js fixture. It separates a written case manifest from the executable driver, uses a loopback HTTP server instead of an external service, records exact runtime versions and SHA-256 source hashes, and compares every observed row against an independent expected row. The fixture was actually executed for this article on Node.js v22.16.0; the observations below are therefore labeled OBSERVED, not hypothetical. The Node.js v22.16.0 global API documentation exposes stable fetch(), AbortSignal.timeout(), AbortSignal.any(), and AbortSignal.reason, which makes that runtime a suitable bounded test target rather than a claim about every browser or JavaScript host.
The acceptance question is deliberately small: for this exact runtime and fixture, does cancellation provenance survive from the winning source, through AbortSignal.any(), to the rejected fetch() value, including already-aborted and timeout paths?
Define the Operational Contract and Release Gate
The contract has three identities, not one boolean. First, the winning source must be determined by the documented composition rule. Second, combined.reason must identify that winner. Third, if fetch() is still unsettled, the rejected value must be the same JavaScript value as the combined reason. The DOM Standard states that a dependent signal copies the abort reason of the source that aborts it; when creating a dependent signal, it scans the supplied list and immediately returns on the first already-aborted signal. The Fetch Standard says an already-aborted request signal aborts the fetch() call with that signal’s abort reason, and later local abort steps do the same.
The release gate is therefore explicit:
ACCEPT when every required case matches the manifest, the fixture exits 0, warnings are empty or reviewed, source hashes are recorded, and the positive and negative controls behave as expected.
REPAIR when the application or test workflow loses provenance even though the runtime behavior matches the documented contract; examples include replacing signal.reason with a generic AbortError, inspecting only .aborted, or reconstructing a new error instead of preserving the winning value.
RERUN when the harness is demonstrably defective, a timing precondition was not established, or a fixture source changed after evidence capture.
HOLD when the target runtime differs from the executed one, a required source is unavailable, a case contradicts the governing contract without a resolved explanation, or the product policy does not say which cancellation owner should win.
Refonte’s guide to JavaScript URL base path resolution validates URL construction before transport, including origin/path boundaries and URL tuples. The same evidence discipline applies here, with reason identity across composition and fetch as the object of validation.
Capture the Baseline and Versioned Environment
Do not begin with the cancellation cases. Begin with the execution envelope, because AbortSignal.any() and built-in fetch() availability are runtime-dependent. Node.js v22.16.0 documents AbortSignal.any(signals) as a composition API whose reason is set from the signal that caused the abort, and documents global fetch() as stable. The fixture execution captured these values directly from the process:
Evidence | OBSERVED value |
Node | v22.16.0 |
npm | 10.9.2 |
Undici reported by process.versions | 6.21.2 |
V8 | 12.4.254.21-node.26 |
OpenSSL | 3.0.16 |
OS / architecture | linux / x64 |
Kernel | Linux 6.18.57 on x86_64 |
Evidence timestamp | 2026-11-07T07:16:19.474Z |
Fixture exit status | 0 |
Warning bytes with --trace-warnings | 0 |
Use commands that fail visibly and retain their output:
node --version
npm --version
node -p 'JSON.stringify(process.versions, null, 2)'
uname -a
NPM_VERSION="$(npm --version)" node --trace-warnings fixture.mjs \
> run.log 2> warnings.log
echo $? > exit-status.txt
sha256sum manifest.json fixture.mjs evidence.json run.log \
warnings.log exit-status.txt > sha256sums.txtThe measured source hashes were 11839b69f19f01f9f368a133308ac0faab1eac66d9f0e1fb5e3766c91ca946cb for manifest.json and 551e8300b8f53ee662b4db663f4ef3322de4a7279d66586e886c380338197d04 for fixture.mjs. Those hashes bind the observations to the exact inputs reported here. They do not make results portable to another Node build, browser, operating system, or future standard revision.
Construct a Disposable Controlled Fixture
The fixture uses only Node built-ins and binds an HTTP server to 127.0.0.1 on an ephemeral port. /ok returns 200; /hang deliberately leaves the response open so cancellation, rather than remote completion, settles the request. No production endpoint, credentials, DNS dependency, proxy, or internet service participates. This isolation matters because a network failure would otherwise be a competing rejection path unrelated to the abort provenance under test.
Refonte’s guide to preserving pending MutationObserver records examines whether queued mutation records are preserved or discarded around disconnect(). It uses lifecycle ordering and an evidence ledger to validate pending-record accounting; the focus here is cancellation source identity.
Pin Inputs and Isolate Side Effects
A specific boundary example is the already-aborted path. Two controllers are aborted before AbortSignal.any() is called. Because the composite is already aborted, the fixture expects zero loopback server hits when that signal is passed to fetch('/hang'). That is a measurable assertion separating “fetch rejected for cancellation” from “a request was sent and later canceled.” The live manual-wins and timeout-wins cases invert that precondition: they wait until the server records exactly one /hang hit before triggering or awaiting the winning cancellation.
The resulting boundary is deterministic enough for diagnosis: already-aborted cases assert serverHits: 0; live cases assert serverHits: 1; the timeout(0) boundary intentionally does not assert hit count because a zero-delay timeout is asynchronous and request dispatch can race the timer task.
Define an Independent Evidence Oracle
The expected-case manifest is authored separately from the runner. The runner does not calculate expected winners from its own control flow; it loads literal expected values and compares only named fields. That separation prevents a common false-positive design in which the same branch chooses the winner and also declares that winner correct.
The core documented oracle is narrow. MDN’s AbortSignal.any() reference says an already-aborted input makes the returned signal already aborted and that the reason comes from the first already-aborted input; for live sources, the reason comes from the first source that aborts. The normative DOM algorithm is stronger evidence: it iterates the sequence and returns immediately when it finds an aborted signal, while later propagation copies a live source’s abort reason to the dependent signal. MDN’s AbortSignal.timeout() reference states that timeout aborts use a TimeoutError DOMException; the DOM Standard likewise specifies a new TimeoutError DOMException when the timeout task signals abort.
The independently authored manifest used for the executed run was:
{
"schema": 1,
"cases": [
{"id":"already-first","expected":{"operation":"reject","winner":"manual-A","reasonType":"Error","sameReasonIdentity":true,"serverHits":0}},
{"id":"already-reversed","expected":{"operation":"reject","winner":"manual-B","reasonType":"Error","sameReasonIdentity":true,"serverHits":0}},
{"id":"already-timeout-first","expected":{"operation":"reject","winner":"timeout","reasonType":"TimeoutError","sameReasonIdentity":true,"serverHits":0}},
{"id":"manual-wins-live","expected":{"operation":"reject","winner":"manual-live","reasonType":"Error","sameReasonIdentity":true,"serverHits":1}},
{"id":"timeout-wins-live","expected":{"operation":"reject","winner":"timeout","reasonType":"TimeoutError","sameReasonIdentity":true,"serverHits":1}},
{"id":"timeout-zero-boundary","expected":{"operation":"reject","winner":"timeout","reasonType":"TimeoutError","sameReasonIdentity":true}},
{"id":"default-abort","expected":{"operation":"reject","winner":"default-abort","reasonType":"AbortError","sameReasonIdentity":true,"serverHits":0}},
{"id":"primitive-reason","expected":{"operation":"reject","winner":"primitive-string","reasonType":"string","sameReasonIdentity":true,"serverHits":0}},
{"id":"negative-control-ok","expected":{"operation":"resolve","status":200,"combinedAborted":false,"reasonType":"undefined","serverHits":1}},
{"id":"empty-any-negative-control","expected":{"operation":"resolve","status":200,"combinedAborted":false,"reasonType":"undefined","serverHits":1}},
{"id":"duplicate-source","expected":{"operation":"reject","winner":"duplicate-manual","reasonType":"Error","sameReasonIdentity":true,"abortEventCount":1,"serverHits":1}}
]
}Reproduce the Misleading Success Case
The misleading success is a check that stops at combined.aborted. If two input signals are already aborted, that boolean is true regardless of which reason won. A test that asserts only true can pass while an application attributes a timeout to user cancellation, or attributes user cancellation to a timeout. The DOM algorithm makes sequence order operationally significant for this already-aborted state: it selects the first aborted signal encountered in the supplied list.
The executed pair proves the distinction. In already-first, controller A and controller B were both aborted before composition; [A, B] produced manual-A, and fetch rejected with the same Error object. In already-reversed, the same two source states were supplied as [B, A]; the winner changed to manual-B, again with exact object identity preserved through fetch. Both cases recorded zero server hits. These are OBSERVED results from the fixture, not claims about untested runtimes.
The operational lesson is not “array order always determines cancellation.” Array order determines which already-aborted source is chosen during composition. For sources that are still live, temporal abort order determines the winner. Conflating those rules is the diagnostic bug this test is designed to expose.
Record the Negative Control Before Repair
The negative control performs a normal fetch while both sources remain live long enough for /ok to complete. Its measurable assertion is operation: resolve, status: 200, combinedAborted: false, reasonType: undefined, and exactly one server hit. OBSERVED: all five fields matched. The empty composition control, AbortSignal.any([]), also allowed /ok to resolve with 200, while the composite remained un-aborted with an undefined reason.
These controls matter because they show that the fixture can observe ordinary completion and does not manufacture a cancellation for every request. A repair that makes all requests reject would fail the negative control even if every cancellation case appeared superficially “consistent.”
Run a Contrasting Correctness Case
The strongest contrast is between two live-source cases with the same transport setup. In manual-wins-live, the fixture creates a manual controller and a 500 ms timeout, starts fetch('/hang'), waits for the loopback server to record the request, then aborts the manual controller with new Error('manual-live'). OBSERVED: combined.reason, the source reason, and the fetch rejection were the same object; the timeout had not aborted at capture time; one request reached the server.
In timeout-wins-live, the fixture creates a manual controller plus AbortSignal.timeout(30), starts the same hanging fetch, waits for the request to arrive, and then lets the timeout settle the race. OBSERVED: the winning reason type was TimeoutError; the same reason object was present on the combined signal and as the fetch rejection; the manual signal was still live; one request reached the server. The timeout type matches the DOM and MDN contract.
Refonte’s guide to preserving large JSON IDs without precision loss also distinguishes successful execution from identity preservation, but its identity is a decimal identifier crossing JSON/Number conversion. This article’s identity is the exact abort reason crossing signal composition and fetch cancellation.
Explore Failure and Boundary Conditions
Boundary cases should challenge the oracle without changing the question. default-abort calls AbortController.abort() with no explicit reason. The DOM Standard specifies an AbortError DOMException as the default abort reason. OBSERVED: Node v22.16.0 produced an AbortError, the composite carried that exact object, fetch rejected with it, and the already-aborted request produced zero server hits.
primitive-reason uses the string "primitive-string". AbortSignal.reason is specified as a JavaScript value rather than as an Error-only slot, and Node’s v22.16.0 documentation types the abort reason as <any>. OBSERVED: the string survived composition and was the exact rejection value from fetch. This is useful as a negative assumption test: code that always reads error.name can lose provenance even when the platform preserves it correctly.
timeout-zero-boundary records timeout.aborted immediately after AbortSignal.timeout(0) construction, then immediately starts fetch. OBSERVED: abortedImmediately was false; the signal later aborted with TimeoutError, and fetch rejected with that same reason. One request happened to reach the local server in this execution, but the manifest deliberately does not make that hit count part of acceptance. The reason is contractual; exact dispatch-before-timer ordering is not the target claim.
Check Missing and Duplicate Evidence
The duplicate-source case composes the same manual signal twice, attaches one abort listener to the composite, starts a hanging fetch, waits for a confirmed server hit, and aborts the source. The measurable assertions are one combined abort event, one server hit, a manual Error winner, and exact reason identity through fetch. OBSERVED: all four matched.
Missing evidence is treated differently from a failed assertion. If the row lacks the source reason description, rejection description, server-hit count where required, or identity result, the release decision is HOLD, not “probably pass.” Duplicate evidence is also rejected operationally: a case ID should appear once in the ledger, and the manifest should contain one expected row for each required case.
Capture Complete Per-Case Evidence
The executable driver below is the version whose SHA-256 hash is reported earlier. It serializes reason type/name/message for review, but acceptance of provenance uses strict identity (===) rather than matching message text. That matters because two independently constructed Error('manual-live') objects are textually similar but are not the same cancellation value.
import http from 'node:http';
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import process from 'node:process';
const root = new URL('./', import.meta.url);
const manifestBytes = await readFile(new URL('manifest.json', root));
const manifest = JSON.parse(manifestBytes);
const rows = [];
let hitSeq = 0;
const hits = [];
const waiters = new Map();
const server = http.createServer((req, res) => {
const hit = { seq: ++hitSeq, url: req.url, method: req.method };
hits.push(hit);
const list = waiters.get(req.url);
if (list) { for (const resolve of list.splice(0)) resolve(hit); }
req.on('aborted', () => { hit.requestAborted = true; });
res.on('close', () => { hit.responseClosed = true; });
if (req.url === '/ok') {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('ok');
}
// /hang intentionally never responds; client cancellation closes it.
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const { port } = server.address();
const base = http://127.0.0.1:${port};
function hitCountSince(start) { return hits.length - start; }
function waitForHit(pathname, afterSeq = 0, ms = 1000) {
const existing = hits.find(h => h.seq > afterSeq && h.url === pathname && !h.claimed);
if (existing) { existing.claimed = true; return Promise.resolve(existing); }
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Errorserver did not receive ${pathname} within ${ms}ms)), ms);
const wrapped = (hit) => { if (hit.seq <= afterSeq) return; hit.claimed = true; clearTimeout(timer); resolve(hit); };
const list = waiters.get(pathname) ?? [];
list.push(wrapped);
waiters.set(pathname, list);
});
}
function describeReason(value) {
if (value === undefined) return { reasonType: 'undefined', name: null, message: null };
if (value === null) return { reasonType: 'null', name: null, message: null };
if (typeof value !== 'object') return { reasonType: typeof value, name: null, message: String(value) };
const name = typeof value.name === 'string' ? value.name : value.constructor?.name ?? 'object';
const message = typeof value.message === 'string' ? value.message : null;
return { reasonType: name, name, message };
}
function reasonType(value) { return describeReason(value).reasonType; }
async function captureFetch(url, signal) {
try {
const response = await fetch(url, { signal });
const body = await response.text();
return { operation: 'resolve', status: response.status, body };
} catch (error) {
return { operation: 'reject', error };
}
}
function compare(expected, observed) {
const mismatches = [];
for (const [key, value] of Object.entries(expected)) {
if (observed[key] !== value) mismatches.push({ key, expected: value, observed: observed[key] });
}
return mismatches;
}
async function runCase(spec) {
const startHits = hits.length;
const id = spec.id;
let combined, result, winner, sourceReason, abortEventCount;
const extra = {};
if (id === 'already-first' || id === 'already-reversed') {
const a = new AbortController(); const b = new AbortController();
const reasonA = new Error('manual-A'); const reasonB = new Error('manual-B');
a.abort(reasonA); b.abort(reasonB);
const ordered = id === 'already-first' ? [a.signal, b.signal] : [b.signal, a.signal];
combined = AbortSignal.any(ordered);
sourceReason = id === 'already-first' ? reasonA : reasonB;
winner = id === 'already-first' ? 'manual-A' : 'manual-B';
result = await captureFetch${base}/hang, combined);
} else if (id === 'already-timeout-first') {
const timeout = AbortSignal.timeout(5);
await new Promise(resolve => timeout.addEventListener('abort', resolve, { once: true }));
const manual = new AbortController(); const manualReason = new Error('manual-already'); manual.abort(manualReason);
combined = AbortSignal.any([timeout, manual.signal]);
sourceReason = timeout.reason; winner = 'timeout';
result = await captureFetch${base}/hang, combined);
} else if (id === 'manual-wins-live') {
const manual = new AbortController(); const timeout = AbortSignal.timeout(500);
combined = AbortSignal.any([manual.signal, timeout]);
const pending = captureFetch${base}/hang, combined);
await waitForHit('/hang', startHits);
sourceReason = new Error('manual-live'); winner = 'manual-live'; manual.abort(sourceReason);
result = await pending;
extra.timeoutAbortedAtCapture = timeout.aborted;
} else if (id === 'timeout-wins-live') {
const manual = new AbortController(); const timeout = AbortSignal.timeout(30);
combined = AbortSignal.any([manual.signal, timeout]);
const pending = captureFetch${base}/hang, combined);
await waitForHit('/hang', startHits);
result = await pending;
sourceReason = timeout.reason; winner = 'timeout';
extra.manualAbortedAtCapture = manual.signal.aborted;
} else if (id === 'timeout-zero-boundary') {
const timeout = AbortSignal.timeout(0);
extra.abortedImmediately = timeout.aborted;
combined = AbortSignal.any([timeout]);
result = await captureFetch${base}/hang, combined);
sourceReason = timeout.reason; winner = 'timeout';
} else if (id === 'default-abort') {
const manual = new AbortController(); manual.abort();
combined = AbortSignal.any([manual.signal]); sourceReason = manual.signal.reason; winner = 'default-abort';
result = await captureFetch${base}/hang, combined);
} else if (id === 'primitive-reason') {
const manual = new AbortController(); manual.abort('primitive-string');
combined = AbortSignal.any([manual.signal]); sourceReason = manual.signal.reason; winner = 'primitive-string';
result = await captureFetch${base}/hang, combined);
} else if (id === 'negative-control-ok') {
const manual = new AbortController(); const timeout = AbortSignal.timeout(500);
combined = AbortSignal.any([manual.signal, timeout]);
result = await captureFetch${base}/ok, combined);
} else if (id === 'empty-any-negative-control') {
combined = AbortSignal.any([]);
result = await captureFetch${base}/ok, combined);
} else if (id === 'duplicate-source') {
const manual = new AbortController(); combined = AbortSignal.any([manual.signal, manual.signal]);
abortEventCount = 0; combined.addEventListener('abort', () => abortEventCount++);
const pending = captureFetch${base}/hang, combined);
await waitForHit('/hang', startHits);
sourceReason = new Error('duplicate-manual'); winner = 'duplicate-manual'; manual.abort(sourceReason);
result = await pending;
} else throw new Errorunknown case: ${id});
const rejection = result.operation === 'reject' ? result.error : undefined;
const observed = {
operation: result.operation,
...(result.status !== undefined ? { status: result.status } : {}),
...(winner ? { winner } : {}),
reasonType: reasonType(combined.reason),
combinedAborted: combined.aborted,
...(result.operation === 'reject' ? { sameReasonIdentity: rejection === combined.reason && combined.reason === sourceReason } : {}),
...(abortEventCount !== undefined ? { abortEventCount } : {}),
serverHits: hitCountSince(startHits),
};
const evidence = {
id,
expected: spec.expected,
observed,
combinedReason: describeReason(combined.reason),
rejectionReason: describeReason(rejection),
sourceReason: describeReason(sourceReason),
...extra,
};
const mismatches = compare(spec.expected, observed);
evidence.verdict = mismatches.length === 0 ? 'PASS' : 'FAIL';
evidence.mismatches = mismatches;
return evidence;
}
for (const spec of manifest.cases) {
try { rows.push(await runCase(spec)); }
catch (error) {
rows.push({ id: spec.id, expected: spec.expected, verdict: 'ERROR', harnessError: describeReason(error), message: error?.stack ?? String(error) });
}
}
await new Promise(resolve => server.close(resolve));
const fixtureBytes = await readFile(new URL('fixture.mjs', root));
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
const output = {
generatedAt: new Date().toISOString(),
runtime: {
node: process.version,
npm: process.env.NPM_VERSION ?? null,
undici: process.versions.undici ?? null,
v8: process.versions.v8,
openssl: process.versions.openssl,
platform: process.platform,
arch: process.arch,
},
hashes: { manifestSha256: sha256(manifestBytes), fixtureSha256: sha256(fixtureBytes) },
summary: {
total: rows.length,
pass: rows.filter(r => r.verdict === 'PASS').length,
fail: rows.filter(r => r.verdict === 'FAIL').length,
error: rows.filter(r => r.verdict === 'ERROR').length,
},
rows,
};
await writeFile(new URL('evidence.json', root), JSON.stringify(output, null, 2) + '\n');
for (const row of rows) console.log(JSON.stringify(row));
console.log(JSON.stringify({ summary: output.summary, hashes: output.hashes, runtime: output.runtime }));
if (output.summary.fail || output.summary.error) process.exitCode = 1;The code block above is the exact fixture source used for the accepted run. Keep the source file and its recorded hash together; any edit, including a diagnostic-only edit, requires a new hash and a new evidence run.
Reconcile Expected and Observed Identities
The executed ledger contained 11 rows: 11 PASS, 0 FAIL, 0 ERROR. That count comes from the generated evidence file in this run. The key reconciliation is below.
Case | Expected provenance | OBSERVED result | Decision |
already-first | first already-aborted source A | A reason; strict identity through fetch; 0 hits | ACCEPT |
already-reversed | first already-aborted source B | B reason; strict identity through fetch; 0 hits | ACCEPT |
already-timeout-first | already-fired timeout first in list | TimeoutError; strict identity; 0 hits | ACCEPT |
manual-wins-live | manual source after request starts | manual Error; strict identity; 1 hit | ACCEPT |
timeout-wins-live | timeout source after request starts | TimeoutError; strict identity; 1 hit | ACCEPT |
timeout-zero-boundary | timeout | initially live, later TimeoutError; strict identity | ACCEPT |
default-abort | default abort reason | AbortError; strict identity; 0 hits | ACCEPT |
primitive-reason | string reason | exact string rejection; 0 hits | ACCEPT |
negative-control-ok | no abort before completion | 200, reason undefined, 1 hit | ACCEPT |
empty-any-negative-control | no source can abort | 200, reason undefined, 1 hit | ACCEPT |
duplicate-source | one logical source | one abort event; strict identity; 1 hit | ACCEPT |
Refonte’s guide to troubleshooting CSS sticky headers also relies on measured state and an acceptance decision. Its measurements concern browser layout geometry and scroll ownership; the reconciled identity here is a cancellation value.
Retain Failure Artifacts Before Rerun
The first internal execution of this fixture exposed a harness defect: the waiter could claim an earlier /hang hit from a prior case, causing the duplicate-source row to show serverHits: 0. That run was not accepted as product evidence. The waiter was repaired to require a hit sequence greater than the case’s starting sequence, then the entire fixture was rerun. The final accepted run is the one bound to the hashes and timestamp reported above.
That distinction is the purpose of RERUN. A harness error does not become a runtime failure merely because it produced a red row. Preserve the rejected run.log, warning log, source hash, and mismatch first; then repair the harness and create a new evidence set. The measurable assertion for rerun safety is that the new fixture hash changes when source changes and that the new evidence file reports only the results of the corrected source.
Separate Documented Behavior From Observation
Documented contract and observed runtime behavior should remain visibly separate. The DOM Standard says AbortSignal.any() creates a dependent signal, first scanning for already-aborted inputs in sequence order; it specifies propagation of the winning source’s reason. It also specifies TimeoutError for AbortSignal.timeout(). The Fetch Standard specifies that an already-aborted request causes the fetch promise to be rejected with the request signal’s abort reason, and that later abort steps abort the fetch call with the same signal reason. Node v22.16.0 documents its AbortSignal.any() reason behavior and stable browser-compatible fetch implementation.
Observation is narrower: in the executed Node v22.16.0 fixture, strict identity survived all eight rejecting provenance cases; the two successful controls resolved; and the duplicate-source composite emitted one observed abort event. Those results do not establish browser interoperability. Web Platform Tests are a useful independent conformance corpus, and the repository contains AbortSignal/fetch tests, but this article did not execute WPT against multiple engines.
There is also a relevant historical warning against generalization: Node has had confirmed AbortSignal.any() implementation bugs in other v22 builds, including an internal assertion report against v22.7.0. That is not evidence of a problem in v22.16.0; it is evidence that exact-version capture matters.
Repair the Workflow Without Masking Errors
If an application currently logs only error.name, the repair is not to synthesize richer names. Preserve the source reason itself and classify it after identity is retained. A robust ownership pattern is to allocate distinct reason objects at the owner boundary, compose signals once, and treat combined.reason as the authoritative winner after abort. When fetch rejects, compare the rejection to combined.reason before mapping it to telemetry or user-visible policy.
const user = new AbortController();
const timeout = AbortSignal.timeout(2_000);
const signal = AbortSignal.any([user.signal, timeout]);
try {
await fetch(url, { signal });
} catch (error) {
if (signal.aborted && error === signal.reason) {
// Provenance is retained; classify signal.reason here.
throw error;
}
// Different failure path: network, parsing, application error, etc.
throw error;
}Do not “repair” a timeout by catching every rejection and throwing a new DOMException('Timeout', 'TimeoutError'). That masks manual cancellation and destroys object identity. Do not infer that a generic AbortError means the timeout did not win; default controller aborts legitimately produce AbortError, while timeouts produce TimeoutError under the tested contract.
Explain the Scope of the Fix
The fix scope is the application’s provenance handling, not the standard algorithm. A specific boundary is a request whose user-cancel controller is already aborted before composition while a timeout is also already aborted. If product policy requires “explicit user cancel wins,” then signal ordering must encode that policy before calling AbortSignal.any(). The measurable assertion is that reversing two already-aborted sources changes the selected reason exactly when the sequence order changes. That happened in the executed already-first/already-reversed pair.
If product policy instead says “timeout wins whenever it has already expired,” put the timeout first and encode that expectation in the manifest. The platform supplies the mechanism; ownership policy remains yours.
Test Recovery, Rollback and Rerun Safety
A safe migration changes one layer at a time. First add evidence without changing cancellation construction. Then introduce AbortSignal.any() behind a local branch or helper. Run the manifest. If provenance fails, roll back the helper while retaining the evidence artifact. Only after the combined path passes should callers remove legacy cancellation plumbing.
Recovery must test that a failed experiment leaves no durable service-side state. This fixture is intentionally read-only at the HTTP layer: /ok returns text and /hang never commits an application action. That keeps rollback safety local. In a real integration test, use an endpoint whose side effects are disposable or transactionally isolated; otherwise “fetch was canceled” does not prove the server stopped work.
Refonte’s guide to HTML number input step-base validation similarly distinguishes observed browser validity from an application-authorized repair, but its rollback surface is form constraints and step-base policy. The present rollback surface is signal composition and cancellation ownership.
A rerun is valid only when the execution envelope is recaptured. Recompute hashes, record versions, truncate or uniquely name prior logs, and require a fresh exit status. If Node, Undici, or fixture source changes, do not merge evidence into the old ledger as though it were one run.
Apply the Acceptance and Ownership Matrix
The final decision for the executed fixture is ACCEPT, bounded to Node.js v22.16.0 on the recorded Linux x64 environment and the two source hashes above. All 11 manifest rows passed; the process exited 0; --trace-warnings produced an empty warning file; already-aborted order, live manual cancellation, live timeout cancellation, zero-delay timeout, default abort, primitive reason, successful controls, and duplicate-source behavior all met the written expectations. This is an acceptance of the fixture contract, not a universal guarantee across runtimes.
Evidence state | Owner | Decision | Required recovery |
All manifest rows match; hashes/version captured | Release owner | ACCEPT | Retain ledger with build/release evidence |
Runtime matches spec-shaped behavior but app rewrites reason | Application owner | REPAIR | Preserve signal.reason; rerun full manifest |
Harness misses its server-hit precondition or source changed | Test owner | RERUN | Retain failed artifacts, repair harness, generate new hashes |
Runtime/version untested or contradiction unresolved | Runtime/release owner | HOLD | Test exact target or resolve divergence before release |
Already-aborted winner conflicts with product ownership policy | Application owner | REPAIR | Reorder sources or redesign ownership explicitly |
Network/service error is confused with cancellation | Application owner | REPAIR | Separate signal.aborted && error === signal.reason from other failures |
Record ACCEPT, REPAIR, RERUN or HOLD
The case-level decision must be machine-derivable from the ledger, while the release decision also considers scope. For this run: 11/11 PASS plus exit 0 and zero warning bytes yields ACCEPT. A single mismatch with valid evidence yields REPAIR if the defect is in application policy, HOLD if it appears to be runtime/spec divergence, or RERUN if fixture integrity is compromised.
The measurable assertion is intentionally not “all requests were canceled.” Two controls must resolve normally, and rejecting cases must preserve the expected reason identity. That asymmetry is what makes the gate diagnostic rather than ceremonial.
Build Related Skills With Refonte Learning
The Frontend Development program page describes a four-month program with a stated dedication of 10–12 hours per week, covering HTML5/CSS3, JavaScript ES6+, React, APIs/AJAX, Git, accessibility and performance topics; it also presents “Mastering JavaScript for Web Development” in the educational path. The same page lists a basic programming understanding as a requirement with HTML/CSS familiarity as a plus, and separately states an admission prerequisite of working toward a bachelor’s or higher-level degree; because those two prerequisite statements are not identical, treat eligibility interpretation as page-specific rather than collapsing them into one rule.
For engineers who want structured practice beyond this single cancellation fixture, that program is a relevant next step because its page explicitly includes JavaScript and API integration. The operational habit to carry forward is the one used here: write the independent oracle first, isolate side effects, capture exact runtime evidence, preserve failure artifacts, and make the release decision from reconciled observations rather than from a green-looking API call. The program page also states that successful completion offers a Training Certificate and Certificate of Internship; any enrollment, internship availability, deadlines, fees, or certificate applicability should be rechecked on the live page at the time of decision.
