Skip to content

CancellableToken

CancellableToken is passed to the async function inside cancellable. It provides utilities for cooperative cancellation: wrapping promises, sleeping, running intervals, animation frames, checking cancellation status, and registering cleanup callbacks.

Internal interface CancellableToken is what the task receives. The caller holds a CancellableHandle.

Root package:

import { CancellableToken } from "@vgerbot/async";

Module subpath:

import { CancellableToken } from "@vgerbot/async/cancellable";

Leaf subpath:

import { CancellableToken } from "@vgerbot/async/cancellable/CancellableToken";
import { cancellable } from "@vgerbot/async";
const handle = cancellable(async (token) => {
// Cancellable sleep — rejects with CancelError if cancelled
await token.sleep(5000);
// Wrap a promise so it rejects on cancellation
const response = await token.wrap(fetch("/api/data"));
// Explicit cancellation check
token.throwIfCancelled();
return response.json();
});
handle.cancel("User navigated away");
class CancellableToken {
readonly signal: AbortSignal;
readonly name: string | undefined;
// Retry state
get retryAttempt: number;
// Cancellation state
isCancelled(): boolean;
throwIfCancelled(): void;
onCancel(callback: (error: CancelError) => void): () => void;
// Promise wrapping
wrap<T>(p: CancellableHandle<T> | Promise<T>): Promise<T>;
// Time utilities
sleep(ms: number): CancellableHandle<void>;
frame(): CancellableHandle<void>;
delay(schedule: (done: () => void) => () => void): CancellableHandle<void>;
interval(fn: () => void | Promise<void>, interval: number): CancellableHandle<void>;
}
Property Type Description
signal AbortSignal The underlying AbortSignal that drives the token’s cancellation. When the parent CancellableHandle is cancelled (or a linked signal/token aborts), this signal aborts. Pass it directly to native APIs that accept AbortSignal, e.g. fetch(url, { signal: token.signal }).
name string | undefined The name assigned to the task (from CancellableOptions.name). Used in cancellation messages and error labels.
retryAttempt number The current retry attempt number (0-indexed). 0 for the initial attempt, 1 for the first retry, and so on. Useful for tracking retry progress when retry options are configured.

Returns true if the token has been cancelled (i.e. the underlying AbortSignal has been aborted).

async (token) => {
if (token.isCancelled()) {
return fallbackValue;
}
// continue work
}

Throws a CancelError if the token has been cancelled. Use this to check for cancellation at strategic points in long-running tasks.

async (token) => {
for (const item of largeArray) {
token.throwIfCancelled();
await processItem(item);
}
}

Registers a callback invoked when the token is cancelled. If the token is already cancelled, the callback is invoked immediately. Returns a cleanup function to remove the listener.

The callback receives the resolved CancelError.

async (token) => {
const connection = await openConnection();
const unsubscribe = token.onCancel((error) => {
console.log("Task cancelled:", error.message);
connection.close();
});
// Later, to remove the listener:
// unsubscribe();
return doWork(connection);
}

Wraps a Promise or CancellableHandle so that it rejects with a CancelError if the token is cancelled.

When wrapping a CancellableHandle, cancellation is forwarded to the nested handle — calling cancel() on the parent will also cancel the wrapped handle.

async (token) => {
// Wrap a plain promise
const response = await token.wrap(fetch("/api/data"));
return response.json();
}
async (token) => {
// Wrap a nested CancellableHandle — cancellation cascades
const handle = cancellable(async (innerToken) => {
await innerToken.sleep(1000);
return "done";
});
const result = await token.wrap(handle);
return result;
}

wrap vs signal Use token.wrap(promise) for promises that don’t natively support AbortSignal. Use token.signal directly with APIs that accept it (e.g., fetch(url, { signal: token.signal })).

Creates a cancellable sleep/delay that resolves after ms milliseconds. Returns a CancellableHandle<void> — if the token is cancelled during the sleep, the handle rejects with a CancelError. The handle can also be cancelled independently of the parent token.

async (token) => {
await token.sleep(1000);
console.log("1 second elapsed");
}

Creates a cancellable animation frame. Uses requestAnimationFrame if available, otherwise falls back to setTimeout (~16ms). Returns a CancellableHandle<void> that resolves on the next animation frame, or rejects with a CancelError if cancelled before the frame callback.

async (token) => {
await token.frame();
// Next animation frame reached
}

Creates a cancellable delay using a custom scheduling function. The scheduling function receives a callback to invoke when the delay completes, and must return a cleanup function to cancel the scheduled operation.

The delay stops when:

  • The parent token is cancelled
  • The returned handle is cancelled
async (token) => {
// Custom delay using setTimeout
await token.delay((done) => {
const timer = setTimeout(done, 1000);
return () => clearTimeout(timer);
});
}
async (token) => {
// Cancel independently
const handle = token.delay((done) => {
const timer = setTimeout(done, 5000);
return () => clearTimeout(timer);
});
handle.cancel();
}

Executes fn repeatedly, waiting interval ms between each execution (after the previous one completes). The function can be synchronous or asynchronous. Returns a CancellableHandle<void> that can be used to cancel the interval independently of the parent token.

The interval stops when:

  • The parent token is cancelled
  • The returned handle is cancelled
  • fn throws an error (non-CancelError errors reject with the original error)

When the interval stops due to cancellation (parent token or handle), the handle resolves with undefined — it does not reject. This allows await handle to complete silently. You can still check handle.isCancelled() to determine if it was cancelled.

When fn throws a non-CancelError error, the handle rejects with that original error.

async (token) => {
// Poll an API every 5 seconds
const handle = token.interval(async () => {
const data = await fetchData();
processData(data);
}, 5000);
// Cancel the interval independently
handle.cancel();
}
const handle = cancellable(async (token) => {
while (true) {
token.throwIfCancelled();
const status = await token.wrap(fetch("/api/status"));
if (status.ok) return status.json();
await token.sleep(1000);
}
});
const handle = cancellable(async (token) => {
// Option 1: Use signal directly (preferred for fetch)
const res = await fetch("/api/data", { signal: token.signal });
// Option 2: Use wrap (for APIs that don't support AbortSignal)
const res2 = await token.wrap(fetch("/api/data"));
return res.json();
});
const handle = cancellable(async (token) => {
const stream = await openStream();
token.onCancel(() => stream.close());
const data = await token.wrap(stream.read());
return data;
});
const handle = cancellable(async (token) => {
// Cancellation cascades to the nested task
const result = await token.wrap(
cancellable(async (innerToken) => {
await innerToken.sleep(1000);
return "inner done";
})
);
return result;
});

CancellableToken is not generic — it doesn’t carry a type parameter. The token is purely for cancellation control.

const task: AsyncTask<MyResult> = async (token: CancellableToken) => {
// token has no type parameter
return myResult;
};