Deferred Promise Tied to AbortSignal
Expose resolve and reject controls for a promise and automatically reject with AbortError when the linked signal fires.
Code
export interface Deferred<Value> {
readonly promise: Promise<Value>;
readonly settled: () => boolean;
resolve(value: Value | PromiseLike<Value>): void;
reject(reason?: unknown): void;
}
export function createDeferred<Value>(signal?: AbortSignal): Deferred<Value> {
if (signal !== undefined && !(signal instanceof AbortSignal)) {
throw new TypeError("signal must be an AbortSignal");
}
let settle: ((value: Value | PromiseLike<Value>) => void) | undefined;
let fail: ((reason?: unknown) => void) | undefined;
let settled = false;
const promise = new Promise<Value>((resolve, reject) => {
settle = resolve;
fail = reject;
});
const markSettled = (): void => {
settled = true;
};
void promise.then(markSettled, markSettled);
const onAbort = (): void => {
if (settled || fail === undefined) return;
settled = true;
fail(new DOMException("The operation was aborted", "AbortError"));
};
if (signal?.aborted === true) {
onAbort();
} else if (signal !== undefined) {
signal.addEventListener("abort", onAbort, { once: true });
const detach = (): void => signal.removeEventListener("abort", onAbort);
void promise.then(detach, detach);
}
return {
promise,
settled: () => settled,
resolve(value) {
if (settled || settle === undefined) return;
settled = true;
settle(value);
},
reject(reason) {
if (settled || fail === undefined) return;
settled = true;
fail(reason);
},
};
}
const controller = new AbortController();
const aborted = createDeferred<number>(controller.signal);
aborted.promise.catch((error: unknown) => {
console.log(error instanceof DOMException ? error.name : "unknown");
});
controller.abort();
console.log(aborted.settled());
const manual = createDeferred<string>();
manual.promise.then((value) => console.log(value));
manual.resolve("done");
manual.resolve("ignored");
console.log(manual.settled());Report a problem with this exampleOpens a pre-filled issue on GitHub.