I'm trying to create a bot on Discord using the Node.js module discord.js, and in one of its commands, it goes through a Map of members in a guild(server) and mutes all of them. Here's the code I'm using:
const Discord = require('discord.js');
const client = new Discord.Client();
const map = new Map();
client.once('ready', () => {
// This is just for demonstration purposes. In the actual code, the values are all different
// members and the keys are their respective IDs. This is inside the "ready" event because
// otherwise the client's cache would be empty.
const hjtunfgb = client.guilds.cache.get('myServerID').members.cache.get('myID');
map.set(0, hjtunfgb);
map.set(1, hjtunfgb);
map.set(2, hjtunfgb);
map.set(3, hjtunfgb);
map.set(4, hjtunfgb);
map.set(5, hjtunfgb);
map.set(6, hjtunfgb);
map.set(7, hjtunfgb);
map.set(8, hjtunfgb);
map.set(9, hjtunfgb);
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', message => {
// Here I have two commands: "foo", which mutes everyone in the Map, and "bar", which does other
// stuff that is not relevant to the problem
if (message.content.toLowerCase() === 'foo') {
map.forEach((member, n) => {
// member.voice.setMute(true) is how the bot mutes people. It returns a promise, that
// rejects if the member is not connected to any voice channel in that server.
member.voice.setMute(true)
.then(() => console.log(n + ' muted!'))
.catch(() => console.error(n + ' error!'));
});
} else if (message.content.toLowerCase() === 'bar') {
console.log('baz');
}
});
client.login('myBotsSecretLoginKey');
However, if a member is not connected to any voice channel, the promise created by the setMute() method will reject, and it is being handled here by logging an error message (again, in the actual code, the rejection is handled properly). The problem is, after rejecting, the forEach halts and does not execute until I execute "foo" again, and then it does execute. For example, if I send "foo" in the chat and disconnect right after "4 muted!" is logged, the output looks like this:
0 muted!
1 muted!
2 muted!
3 muted!
4 muted!
5 error!
If I connect or disconnect to the voice channel, nothing happens, and if I write "bar" the program outputs "baz" normally. Then, if I join a voice channel and send "foo" again, the following is outputted:
0 muted!
6 muted!
7 muted!
8 muted!
9 muted!
1 muted!
2 muted!
3 muted!
4 muted!
5 muted!
6 muted!
7 muted!
8 muted!
9 muted!
So, the first forEach command is called on the Map, which triggers the previous forEach to resume operation. If, instead, I call "foo" and then join in and call "foo" a third time, on the former call 0 error!
is outputted and on the latter the output is
0 muted!
6 muted!
...
9 muted!
1 muted!
...
9 muted!
1 muted!
...
9 muted!
So, the forEach halts on a promise rejection, even if the rejection is handled. But I need the forEach to resume immediately, as the bot will be constantly muting and unmuting people, and one disconnected member mustn't block other members from being muted. How can I solve this?
EDIT: Making the callback async and wrapping setMute in a try...catch block didn't solve the issue. And, as someone pointed out, the forEach is indeed running to completion before any promise rejects - I checked this by console.logging before running .setMute(). So, the problem is with how the promises are being handled by node, discord.js, or Discord itself. Could someone provide information on why this is happening and how to avoid this Promise handling halt?
I also tried using for...of and using an array instead of a Map, but they all yielded the same problem.
EDIT 2: I was able to solve the problem by changing the callback in the "message" event listener as follows:
client.on('message', async message => {
if (message.content.toLowerCase() === 'foo') {
for (const [n, member] of map) {
try {
await member.voice.setMute(true);
console.log(n + ' muted!');
} catch (error) {
console.error(n + ' error!');
}
}
} else if (message.content.toLowerCase() === 'bar') {
console.log('baz');
}
});
This way, the code waits for the Promise to resolve before creating the next Promise. This isn't the most optimal way, since these Promises could run in parallel, but Discord deals with these Promises one at a time regardless so in practice it won't affect performance. I tried running the Promises in parallel without using forEach using the following setup:
client.on('message', message => {
if (message.content.toLowerCase() === 'foo') {
Promise.all(Array.from(map.entries()).map(async ([n, member]) => {
try {
await member.voice.setMute(true);
console.log(n + ' muted!');
} catch (error) {
console.error(n + ' error!');
}
}));
} else if (message.content.toLowerCase() === 'bar') {
console.log('baz');
}
});
But it had the very same problem. It seems like the problem is on how node, discord.js, and/or Discord handles multiple Promises being requested at the same time. What can I do to fix this halt? Running the asynchronous code synchronously will work for now, but running them in parallel would be better in the long run. Should I report this as a bug to discord.js and/or Discord, or there is a purpose (and a fix) for this that I am unaware of?