Overview

ZeroPromise<T> is a Promise you can cancel. It starts active, then settles to resolved, rejected, or cancelled. .signal is an AbortSignal. await, .then, .catch, and .finally work like a normal Promise.

Get, Post, Put, Patch, Delete, and Fetch return a ZeroPromise. Call .cancel() on that handle to abort the request.

asyncAtom passes the current run as { zero }. Pass zero.signal into fetch. asyncAction and taskAction watch a ZeroPromise you return: their cancel() / stop() call .cancel() on it. They recognize it by __isZeroPromise__.

ZeroPromise vs Promise

API
Cancel
State
Promise No built-in cancel Settles once; no AbortSignal.
ZeroPromise .cancel() + .signal isActive / isResolved / isRejected / isCancelled.

Resolve vs cancel

Start a 2s ZeroPromise, then cancel before it resolves. Status tracks active / resolved / cancelled.

import { html, atom, ZeroPromise, renderApp } from 'mates';

const App = () => {
const status = atom('idle');
const result = atom('');
let current = null;

const start = () => {
current = new ZeroPromise();
status.set('active');
result.set('');
const timer = setTimeout(() => {
if (current && current.isActive()) {
current.resolve('finished after 2s');
}
}, 2000);
current
.then((v) => { status.set('resolved'); result.set(v); })
.catch((e) => {
clearTimeout(timer);
status.set(e.name === 'AbortError' ? 'cancelled' : 'rejected');
result.set(e.message || String(e));
});
};

const cancel = () => { current?.cancel(); };

return () => html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${start} ?disabled=${status() === 'active'}>
Start 2s work
</button>
<button class="btn-danger" @click=${cancel} ?disabled=${status() !== 'active'}>
Cancel
</button>

AbortSignal + fetch

Pass zero.signal into fetch. Cancel aborts the network request.

import { html, atom, ZeroPromise, renderApp } from 'mates';

const App = () => {
const note = atom('Idle');
let current = null;

const run = () => {
current = new ZeroPromise();
note.set('fetching…');
fetch('https://jsonplaceholder.typicode.com/todos/1', {
signal: current.signal,
})
.then((r) => r.json())
.then((todo) => {
current.resolve(todo.title);
note.set(todo.title);
})
.catch((e) => {
if (e.name === 'AbortError') note.set('aborted');
else note.set(e.message);
current.reject(e);
});
};

return () => html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${run}>Fetch with signal</button>
<button class="btn-danger" @click=${() => current?.cancel()}>Abort</button>
</div>
<p class="muted">${note()}</p>
</div>
`;
};
renderApp(App, document.getElementById('app'));

Cancel a Get()

HTTP helpers already return a ZeroPromise. cancel() aborts that request with AbortError.

import { html, atom, Get, renderApp } from 'mates';

const App = () => {
const note = atom('idle');
let req = null;

const load = () => {
note.set('loading');
req = Get('https://jsonplaceholder.typicode.com/todos/1');
req
.then((todo) => note.set(todo.title))
.catch((e) => note.set(e.name === 'AbortError' ? 'cancelled' : e.message));
};

return () => html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${load}>Get todo</button>
<button class="btn-danger" @click=${() => req?.cancel()}>Cancel</button>
</div>
<p class="muted">${note()}</p>
</div>
`;
};
renderApp(App, document.getElementById('app'));