Event 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 Clientopen in new window documentation to see the full list of events.

TIP

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.

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

client.once(Events.ClientReady, readyClient => {
	console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
1
2
3
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 });
		}
	}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Currently, the event listeners are in the index.js file. Client#event:readyopen in new window emits once when the Client becomes ready for use, and Client#event:interactionCreateopen in new window 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.

WARNING

You're only going to move these two events from index.js. The code for loading command files will stay here!

Individual event files

Your project directory should look something like this:

discord-bot/
├── commands/
├── node_modules/
├── config.json
├── deploy-commands.js
├── index.js
├── package-lock.json
└── 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.

const { Events } = require('discord.js');

module.exports = {
	name: Events.ClientReady,
	once: true,
	execute(client) {
		console.log(`Ready! Logged in as ${client.user.tag}`);
	},
};
1
2
3
4
5
6
7
8
9
const { Events } = require('discord.js');

module.exports = {
	name: Events.InteractionCreate,
	async execute(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 });
			}
		}
	},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, GatewayIntentBits } = require('discord.js');
const { token } = require('./config.json');

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

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);
		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.`);
		}
	}
}

client.login(token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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.

Reading 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.readdirSync().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'].

const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, GatewayIntentBits } = require('discord.js');
const { token } = require('./config.json');

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

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);
		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.`);
		}
	}
}

const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));

for (const file of eventFiles) {
	const filePath = path.join(eventsPath, file);
	const event = require(filePath);
	if (event.once) {
		client.once(event.name, (...args) => event.execute(...args));
	} else {
		client.on(event.name, (...args) => event.execute(...args));
	}
}

client.login(token);

























 
 
 
 
 
 
 
 
 
 
 
 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

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 Clientopen in new window class in discord.js extends the EventEmitteropen in new window class. Therefore, the client object exposes the .on()open in new window and .once()open in new window 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 syntaxopen in new window, then calls event.execute() while passing in the args array using the ... spread syntaxopen in new window. 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.

TIP

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.

Resulting code

If you want to compare your code to the code we've constructed so far, you can review it over on the GitHub repository here open in new window.