Command handling
Unless your bot project is small, it's not a very good idea to have a single file with a giant
if
/
else if
chain for commands. If you want to implement features into your bot and make your development process a lot less painful, you'll want to implement a command handler. Let's get started on that!
TIP
For fully functional slash commands, there are three important pieces of code that need to be written. They are:
- The individual command files , containing their definitions and functionality.
- The command handler, which dynamically reads the files and executes the commands.
- The command deployment script , to register your slash commands with Discord so they appear in the interface.
These steps can be done in any order, but all are required before the commands are fully functional.
This page details how to complete Step 2 . Make sure to also complete the other pages linked above!
Loading command files
Now that your command files have been created, your bot needs to load these files on startup.
In your
index.js
file, make these additions to the base template:
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Events, GatewayIntentBits } = require('discord.js');
const { token } = require('./config.json');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.commands = new Collection();
2
3
4
5
6
7
8
We recommend attaching a
.commands
property to your client instance so that you can access your commands in other files. The rest of the examples in this guide will follow this convention. For TypeScript users, we recommend extending the base Client class to add this property,
casting
open in new window
, or
augmenting the module type
open in new window
.
TIP
-
The
fs
open in new window module is Node's native file system module.fs
is used to read thecommands
directory and identify our command files. -
The
path
open in new window module is Node's native path utility module.path
helps construct paths to access files and directories. One of the advantages of thepath
module is that it automatically detects the operating system and uses the appropriate joiners. -
The
Collection
open in new window class extends JavaScript's nativeMap
open in new window class, and includes more extensive, useful functionality.Collection
is used to store and efficiently retrieve commands for execution.
Next, using the modules imported above, dynamically retrieve your command files with a few more additions to the
index.js
file:
client.commands = new Collection();
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
for (const folder of commandFolders) {
const commandsPath = path.join(foldersPath, folder);
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
// Set a new item in the Collection with the key as the command name and the value as the exported module
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
First,
path.join()
open in new window
helps to construct a path to the
commands
directory. The first
fs.readdirSync()
open in new window
method then reads the path to the directory and returns an array of all the folder names it contains, currently
['utility']
. The second
fs.readdirSync()
method reads the path to this directory and returns an array of all the file names they contain, currently
['ping.js', 'server.js', 'user.js']
. To ensure only command files get processed,
Array.filter()
removes any non-JavaScript files from the array.
With the correct files identified, the last step is dynamically set each command into the
client.commands
Collection. For each file being loaded, check that it has at least the
data
and
execute
properties. This helps to prevent errors resulting from loading empty, unfinished, or otherwise incorrect command files while you're still developing.
Receiving command interactions
You will receive an interaction for every slash command executed. To respond to a command, you need to create a listener for the
Client#interactionCreate
open in new window
event that will execute code when your application receives an interaction. Place the code below in the
index.js
file you created earlier.
client.on(Events.InteractionCreate, interaction => {
console.log(interaction);
});
2
3
Not every interaction is a slash command (e.g.
MessageComponent
interactions). Make sure to only handle slash commands in this function by making use of the
BaseInteraction#isChatInputCommand()
open in new window
method to exit the handler if another type is encountered. This method also provides typeguarding for TypeScript users, narrowing the type from
BaseInteraction
to
ChatInputCommandInteraction
open in new window
.
client.on(Events.InteractionCreate, interaction => {
if (!interaction.isChatInputCommand()) return;
console.log(interaction);
});
2
3
4
Executing commands
When your bot receives a
Client#interactionCreate
open in new window
event, the interaction object contains all the information you need to dynamically retrieve and execute your commands!
Let's take a look at the
ping
command again. Note the
execute()
function that will reply to the interaction with "Pong!".
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction) {
await interaction.reply('Pong!');
2
3
4
5
6
7
8
First, you need to get the matching command from the
client.commands
Collection based on the
interaction.commandName
. Your
Client
open in new window
instance is always available via
interaction.client
. If no matching command is found, log an error to the console and ignore the event.
With the right command identified, all that's left to do is call the command's
.execute()
method and pass in the
interaction
variable as its argument. In case something goes wrong, catch and log any error to the console.
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });