AbortController patterns
AbortController and AbortSignal are full WHATWG implementations in Phasis. The standard composition helpers (AbortSignal.any, AbortSignal.timeout) ship too. This page covers the patterns you actually reach for and how cancellation propagates across the PHP boundary.
Cancelling a fetch
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort(); // p rejects with AbortError DOMExceptionThe reason is AbortError by default, but you can supply your own:
ctrl.abort(new Error('user cancelled'));fetch rejects with whatever you passed.
Timeout
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });AbortSignal.timeout(ms) returns a signal that aborts itself after ms. The timer fires through the engine's event loop, the same one that drives setTimeout / queueMicrotask (see API: Event loop). The fetch transport polls the signal during transfer — for the default CurlTransport, via CURLOPT_PROGRESSFUNCTION — so the request is aborted promptly once the deadline lands.
The loop is virtual-time by default: it advances its clock to the next pending deadline rather than sleeping PHP, so a fetch(url, { signal: AbortSignal.timeout(5000) }) either completes before the deadline or aborts on the next loop tick — it never makes PHP wait. Real-time pacing needs the host to drive tickEventLoop() against a wall clock.
Race: first cancellation wins
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(10_000)]);
const res = await fetch(url, { signal });
// res rejects if EITHER the user clicks cancel OR 10s elapsesPropagation through layers
Wrap your own async work with a signal-aware loop:
async function fetchPaged(url, signal) {
const out = [];
for (let page = 1; ; page++) {
if (signal.aborted) throw signal.reason;
const r = await fetch(`${url}?page=${page}`, { signal });
const data = await r.json();
if (data.length === 0) break;
out.push(...data);
}
return out;
}The fetch inherits the signal directly; the manual if (signal.aborted) covers gaps between requests where the page-loop would otherwise keep running.
Listening for the abort event
ctrl.signal.addEventListener('abort', () => {
releaseDatabaseLock();
cancelDownstreamWork();
});signal.reason is set before listeners fire.
Signal in a custom fetch transport
If you swap the transport via setFetchTransport(), the signal is passed to your callable as a JsObject. Honor it by polling:
$engine->setFetchTransport(function (array $req, $signal) {
$ch = curl_init($req['url']);
// ...
if ($signal !== null) {
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
fn () => $signal->get('aborted')->value ? 1 : 0);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
}
$body = curl_exec($ch);
if (curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK) {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'aborted', 'aborted',
);
}
// ...
});Throwing TransportException with kind: 'aborted' makes the JS-side fetch reject with AbortError DOMException — same as if the spec abort-loop had run.
For backends with native cancellation (Guzzle's on_progress, Symfony's ResponseInterface::cancel), wire the same poll-and-throw shape.
Patterns that won't work yet
signal.addEventListener('abort', () => fetch(cleanupUrl))— listener firing is synchronous; the cleanup fetch starts but won't complete inside the abort dispatch. Schedule it via the surroundingawait.
setTimeout(() => ctrl.abort(), N) works (the timer family ships and is WPT-covered), but AbortSignal.timeout(N) is shorter and conveys intent better when all you want is an abort-after-deadline signal.
See also
- Fetch transport — full request/response/signal interface for custom transports.
- Web APIs (WPT) — AbortController/AbortSignal WPT pass rate.
Cookie jar
Phasis fetch is cookieless by default. Mount a jar via setCookieJar() to persist cookies across requests. Two methods — get(url) and set(url, header) — and any object that responds to them works.
Overview
How Phasis compares to V8 (Node, Chrome), SpiderMonkey, and JavaScriptCore. test262 conformance, Web Platform Tests pass rates, real-world library byte-equality, and known limitations.