Skip to main content
Version: v2.10.0

Command handlingโ€‹

Unless your bot is very small, a single file with a giant if/else if chain for commands is hard to maintain. A command handler lets you split each command into its own file and load them dynamically.

note

For fully functional commands, three pieces need to be in place:

  1. Individual command files with their definitions and functionality.
  2. The command handler on this page, which dynamically reads and executes those files.
  3. A deployment script to register commands with Discord.

These can be completed in any order. All three are required before slash commands are functional. For message commands, only the first two are required. This page covers step 2.


Loading command filesโ€‹

note

This page requires a valid main file and at least one command created following these steps. Select the tab below for the type of commands you want to handle.

Now that your command files have been created, your bot needs to load them on startup.

Make the following additions to your index.js:

const fs = require("node:fs");
const path = require("node:path");
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require("discord.js");
const { Manager, SearchPlatform, AutoPlayPlatform, TrackPartial } = require("magmastream");
const config = require("./config.js");

const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
],
});

client.manager = new Manager({
playNextOnEnd: true,
enablePriorityMode: false,
normalizeYouTubeTitles: true,
trackPartial: [
TrackPartial.Author,
TrackPartial.ArtworkUrl,
TrackPartial.Duration,
TrackPartial.Identifier,
TrackPartial.PluginInfo,
TrackPartial.Requester,
TrackPartial.SourceName,
TrackPartial.Title,
TrackPartial.Track,
TrackPartial.Uri,
],
defaultSearchPlatform: SearchPlatform.YouTube,
autoPlaySearchPlatforms: [AutoPlayPlatform.Spotify, AutoPlayPlatform.Deezer, AutoPlayPlatform.YouTube],
nodes: config.nodes,
send: (packet) => {
const guild = client.guilds.cache.get(packet.d.guild_id);
if (guild) guild.shard.send(packet);
},
});

// Store commands
client.commands = new Collection();

client.login(config.token);

We attach a .commands property to the client so commands are accessible across all files.

note
  • fs โ€” Node's native file system module, used to read the commands directory.
  • path โ€” Node's path utility, used to build cross-platform file paths.
  • Collection โ€” extends JavaScript's native Map, used to store and retrieve commands efficiently.

Next, dynamically load your command files:

client.commands = new Collection();

const foldersPath = path.join(__dirname, "commands");

const loadCommands = (folderPath) => {
const commandFiles = fs.readdirSync(folderPath);

for (const file of commandFiles) {
const filePath = path.join(folderPath, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
loadCommands(filePath);
} else if (file.endsWith(".js")) {
const command = require(filePath);
if ("data" in command && "execute" in command) {
console.log(`[INFO] Loaded command: ${command.data.name}`);
client.commands.set(command.data.name, command);
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
};

loadCommands(foldersPath);

loadCommands() recursively scans the commands folder. For each .js file it finds, it checks for the required data and execute properties before adding it to the client.commands collection. Invalid files log a warning instead of crashing.


Receiving command interactionsโ€‹

Add a listener for the interactionCreate event. Use isChatInputCommand() to filter out non-command interactions:

client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;

const command = client.commands.get(interaction.commandName);

if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}

try {
await command.execute(client, interaction);
} catch (error) {
console.error(error);

if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: "There was an error while executing this command!", flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: "There was an error while executing this command!", flags: MessageFlags.Ephemeral });
}
}
});

Next stepsโ€‹

  1. Create an event handler to clean up your index.js.
  2. Create a deployment script to register your slash commands with Discord.

Resulting codeโ€‹

You can review the complete example on the GitHub repository here .