How can I use AbortController to cancel a Discord.js collector or timeout gracefully?
0 reputation · 06 Oct 2023, 16:12 UTC
0 reputation · 06 Oct 2023, 16:12 UTC
When building a Discord.js command that creates a MessageCollector with a set time limit, I sometimes need to stop the collector early because an external condition occurs, such as a button interaction that indicates the user no longer wants to wait. The collector’s internal timeout relies on Node.js timers, and the timers/promises API offers an AbortSignal option to cancel the scheduled timeout.
Although AbortController provides an abort() method to signal cancellation, it is not obvious how to forward its signal to the collector’s timeout mechanism so that the collector stops cleanly without leaving pending timers or causing unhandled promise rejections. I want to understand the correct way to integrate the signal and what guarantees the library provides regarding cleanup.
Questions: How do I attach the AbortSignal to the MessageCollector’s timeout? What is the state of the collector’s promise after an abort? Is any manual cleanup required to prevent lingering timers?Discord.js MessageCollector does not expose the Node timer it uses for the timeout option, so you cannot directly attach an AbortSignal to it. The recommended approach is to listen for the external abort event and call collector.stop() yourself. collector.stop() emits the end event, cleans up its internal timer and resolves the collector’s promise, leaving no dangling timers.
const collector = message.channel.createMessageCollector({
filter: m => m.author.id === user.id,
max: 5,
time: 30000 // 30 s
});
const abortController = new AbortController();
const abortSignal = abortController.signal;
abortSignal.addEventListener('abort', () => {
collector.stop('User cancelled');
});
// e.g. button interaction
button.on('click', () => abortController.abort());
collector
.promise
.then(collected => {
console.log('Collected', collected.size, 'messages');
})
.catch(err => console.error(err));
Because collector.stop() clears the internal timeout, no manual clearTimeout call is needed. If the collector has already ended when abort() fires, the listener simply does nothing.
abort(), the collector’s ended property is true.end event is emitted with the reason passed to stop.process._getActiveHandles() after the collector ends.If you ever need to re‑enable collection after an abort, you must create a new MessageCollector instance.
Use comments to ask for clarification. Post a solution as an answer.
6,830 reputation · 07 Oct 2023, 01:21 UTC
The manual abortSignal.addEventListener('abort', () => collector.stop()) pattern works, but it is only necessary on older releases. In Discord.js v14.11+ the collector constructors accept signal?: AbortSignal in the options, so passing signal: controller.signal makes the collector stop automatically when controller.abort() is called.
That means the end event still fires and internal timers are cleared by the library, but the abort is irreversible for that signal. A new AbortController and collector are required to restart collection, and any user-added listeners on the collector or channel should be removed in the end handler to avoid leaks.
Verify by checking the type definitions for signal on the collector options and confirming typeof AbortController !== 'undefined' in your Node runtime.