Consider this simple example, probably a function you wrote a couple of times, but now abortable:
/**
*
* @param {number} delay
* @param {AbortSignal} [abortSignal]
* @returns {Promise<void>}
*/
export default function timeoutPromise(delay, abortSignal) {
return new Promise((resolve, reject) => {
if(abortSignal) {
abortSignal.throwIfAborted();
}
const timeout = setTimeout(() => {
resolve();
}, delay);
abortSignal.addEventListener("abort", () => {
clearTimeout(timeout);
reject(new Error("Aborted"));
});
});
}
The obvious issue is that this will not clear the eventListener if the timeout succeeds normally. It can be done, but it is quite ugly:
/**
*
* @param {number} delay
* @param {AbortSignal} [abortSignal]
* @returns {Promise<void>}
*/
export default function timeoutPromise(delay, abortSignal) {
return new Promise((resolve, reject) => {
// note: changed to reject() to get consistent behavior regardless of the signal state
if(abortSignal && abortSignal.aborted) {
reject(new Error("timeoutPromise aborted"));
}
let timeout = null;
function abortHandler() {
clearTimeout(timeout);
reject(new Error("timeoutPromise aborted"))
}
timeout = setTimeout(() => {
if(abortSignal) {
abortSignal.removeEventListener("abort", abortHandler);
}
resolve();
}, delay);
if(abortSignal) {
abortSignal.addEventListener("abort", abortHandler, {once: true});
}
});
}
That's... a lot of code for such a simple thing. Am I doing this right or is there a better way?
You can use optional chaining for all the method calls on the AbortSignal
and it becomes more straightforward:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
function done() {
resolve();
signal?.removeEventListener("abort", stop);
}
function stop() {
reject(this.reason);
clearTimeout(handle);
}
signal?.throwIfAborted();
const handle = setTimeout(done, ms);
signal?.addEventListener("abort", stop);
});
}
(from my answer to How to cancel JavaScript sleep?)
Or do only a single check for the signal existence:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
if (!signal) {
setTimeout(resolve, ms);
return;
}
function done() {
resolve();
signal.removeEventListener("abort", stop);
}
function stop() {
reject(this.reason);
clearTimeout(handle);
}
signal.throwIfAborted();
const handle = setTimeout(done, ms);
signal.addEventListener("abort", stop);
});
}
which you can of course golf further:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
if (!signal) return setTimeout(resolve, ms);
signal.throwIfAborted();
const handle = setTimeout(() => {
resolve();
signal.removeEventListener("abort", stop);
}, ms);
const stop = () => {
reject(signal.reason);
clearTimeout(handle);
};
signal.addEventListener("abort", stop);
});
}