Section titled Event handlingEvent handling

Node.js uses an event-driven architecture, making it possible to execute code when a specific event occurs. The discord.js library takes full advantage of this. You can visit the Client documentation to see the full list of events.

This page assumes you've followed the guide up to this point, and created your index.js and individual slash commands according to those pages.

Tip

At this point, your index.js file has code for loading commands, and listeners for two events: ClientReady and InteractionCreate.

Commands
ClientReady
InteractionCreate

_19
const commands = new Collection();
_19
_19
const foldersPath = fileURLToPath(new URL('commands', import.meta.url));
_19
const commandFolders = await readdir(foldersPath);
_19
_19
for (const folder of commandFolders) {
_19
const commandsPath = join(foldersPath, folder);
_19
const commandFiles = await readdir(commandsPath).then((files) => files.filter((file) => file.endsWith('.js')));
_19
for (const file of commandFiles) {
_19
const filePath = join(commandsPath, file);
_19
const command = await import(filePath);
_19
// Set a new item in the Collection with the key as the command name and the value as the exported module
_19
if ('data' in command && 'execute' in command) {
_19
commands.set(command.data.name, command);
_19
} else {
_19
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
_19
}
_19
}
_19
}

Currently, all of this code is in the index.js file. Client#ready emits once when the Client becomes ready for use, and Client#interactionCreate emits whenever an interaction is received. Moving the event listener code into individual files is simple, and we'll be taking a similar approach to the command handler.

Section titled Individual event filesIndividual event files

Your project directory should look something like this:


_10
discord-bot/
_10
├── commands/
_10
├── node_modules/
_10
├── config.json
_10
├── deploy-commands.js
_10
├── index.js
_10
├── package-lock.json
_10
└── package.json

Create an events folder in the same directory. You can then move the code from your event listeners in index.js to separate files: events/ready.js and events/interactionCreate.js. The InteractionCreate event is responsible for command handling, so the command loading code will move here too.

events/interactionCreate.js
events/ready.js
index.js

_46
import { readdir } from 'node:fs/promises';
_46
import { join } from 'node:path';
_46
import { fileURLToPath } from 'node:url';
_46
import { Collection, Events } from 'discord.js';
_46
_46
const commands = new Collection();
_46
_46
const foldersPath = fileURLToPath(new URL('commands', import.meta.url));
_46
const commandFolders = await readdir(foldersPath);
_46
_46
for (const folder of commandFolders) {
_46
const commandsPath = join(foldersPath, folder);
_46
const commandFiles = await readdir(commandsPath).then((files) => files.filter((file) => file.endsWith('.js')));
_46
for (const file of commandFiles) {
_46
const filePath = join(commandsPath, file);
_46
const command = await import(filePath);
_46
// Set a new item in the Collection with the key as the command name and the value as the exported module
_46
if ('data' in command && 'execute' in command) {
_46
commands.set(command.data.name, command);
_46
} else {
_46
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
_46
}
_46
}
_46
}
_46
_46
export const data = {
_46
name: Events.InteractionCreate,
_46
};
_46
_46
export async function execute(interaction) {
_46
if (!interaction.isChatInputCommand()) return;
_46
_46
const command = commands.get(interaction.commandName);
_46
_46
if (!command) {
_46
console.error(`No command matching ${interaction.commandName} was found.`);
_46
return;
_46
}
_46
_46
try {
_46
await command.execute(interaction);
_46
} catch (error) {
_46
console.error(`Error executing ${interaction.commandName}`);
_46
console.error(error);
_46
}
_46
}

The name property states which event this file is for, and the once property holds a boolean value that specifies if the event should run only once. You don't need to specify this in interactionCreate.js as the default behavior will be to run on every event instance. The execute function holds your event logic, which will be called by the event handler whenever the event emits.

Section titled Reading event filesReading event files

Next, let's write the code for dynamically retrieving all the event files in the events folder. We'll be taking a similar approach to our command handler. Place the new code highlighted below in your index.js.

fs.readdir() combined with array.filter() returns an array of all the file names in the given directory and filters for only .js files, i.e. ['ready.js', 'interactionCreate.js'].


_22
import { readdir } from 'node:fs/promises';
_22
import { join } from 'node:path';
_22
import { fileURLToPath } from 'node:url';
_22
import { Client, GatewayIntentBits } from 'discord.js';
_22
import config from './config.json' assert { type: 'json' };
_22
_22
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
_22
_22
const eventsPath = fileURLToPath(new URL('events', import.meta.url));
_22
const eventFiles = await readdir(eventsPath).then((files) => files.filter((file) => file.endsWith('.js')));
_22
_22
for (const file of eventFiles) {
_22
const filePath = join(eventsPath, file);
_22
const event = await import(filePath);
_22
if (event.data.once) {
_22
client.once(event.data.name, (...args) => event.execute(...args));
_22
} else {
_22
client.on(event.data.name, (...args) => event.execute(...args));
_22
}
_22
}
_22
_22
client.login(config.token);

You'll notice the code looks very similar to the command loading above it - read the files in the events folder and load each one individually.

The Client class in discord.js extends the EventEmitter class. Therefore, the client object exposes the .on() and .once() methods that you can use to register event listeners. These methods take two arguments: the event name and a callback function. These are defined in your separate event files as name and execute.

The callback function passed takes argument(s) returned by its respective event, collects them in an args array using the ... rest parameter syntax, then calls event.execute() while passing in the args array using the ... spread syntax. They are used here because different events in discord.js have different numbers of arguments. The rest parameter collects these variable number of arguments into a single array, and the spread syntax then takes these elements and passes them to the execute function.

After this, listening for other events is as easy as creating a new file in the events folder. The event handler will automatically retrieve and register it whenever you restart your bot.

In most cases, you can access your client instance in other files by obtaining it from one of the other discord.js structures, e.g. interaction.client in the InteractionCreate event. You do not need to manually pass it to your events.

Tip

Section titled Resulting codeResulting code