Skip to main content
Events allow your bot to respond to actions on Discord like messages, member joins, reactions, and more. CommandKit provides a structured way to organize and handle events.

Basic Event Handler

Create an event directory matching the Discord.js event name:
1

Create event directory

Create a folder in src/app/events/ with the exact Discord.js event name:
mkdir -p src/app/events/clientReady
2

Create handler file

Add a handler file in the event directory:
import type { EventHandler } from 'commandkit';
import { Logger } from 'commandkit/logger';

const handler: EventHandler<'clientReady'> = async (client) => {
  Logger.info(`Logged in as ${client.user.username}!`);
  Logger.info(`Serving ${client.guilds.cache.size} guilds`);
};

export default handler;
3

Start your bot

The handler automatically runs when the clientReady event fires.

Available Discord.js Events

Common Discord.js events you can handle:
  • clientReady - Bot comes online
  • messageCreate - User sends a message
  • messageUpdate - Message is edited
  • messageDelete - Message is deleted
  • guildMemberAdd - User joins a server
  • guildMemberRemove - User leaves a server
  • interactionCreate - User creates an interaction (handled internally by CommandKit)
  • messageReactionAdd - Reaction added to message
  • voiceStateUpdate - User joins/leaves/changes voice channel
See the full list in the Discord.js documentation.

Multiple Handlers Per Event

Create multiple handlers for the same event by adding more files:
src/app/events/messageCreate/
├── 01-give-xp.ts
├── 02-anti-spam.ts
└── 03-log-message.ts
import type { EventHandler } from 'commandkit';

const handler: EventHandler<'messageCreate'> = async (message) => {
  // Ignore bots
  if (message.author.bot) return;
  // Ignore DMs
  if (!message.guildId) return;

  // Award XP logic
  const xpEarned = Math.floor(Math.random() * 10) + 15;
  await awardXP(message.author.id, message.guildId, xpEarned);
};

export default handler;

async function awardXP(userId: string, guildId: string, xp: number) {
  // Implementation here
  console.log(`Awarded ${xp} XP to ${userId} in ${guildId}`);
}
Handlers execute in alphabetical order by filename. Use number prefixes (01-, 02-) to control execution order.

Events with Multiple Parameters

Some events pass multiple parameters:
src/app/events/messageUpdate/log-edits.ts
import type { EventHandler } from 'commandkit';
import { Logger } from 'commandkit/logger';

const handler: EventHandler<'messageUpdate'> = async (oldMessage, newMessage) => {
  // Ignore bot messages
  if (newMessage.author?.bot) return;

  // Ignore if content didn't change
  if (oldMessage.content === newMessage.content) return;

  Logger.info(
    `Message edited in ${newMessage.guild?.name}:\n` +
    `Before: ${oldMessage.content}\n` +
    `After: ${newMessage.content}`
  );
};

export default handler;

Run Once Events

Use export const once = true for events that should only run once:
src/app/events/clientReady/setup-database.ts
import type { EventHandler } from 'commandkit';
import { prisma } from '../../database';

export const once = true;

const handler: EventHandler<'clientReady'> = async (client) => {
  // Run database migrations
  await prisma.$connect();
  console.log('Database connected');

  // Sync guild data
  for (const guild of client.guilds.cache.values()) {
    await prisma.guild.upsert({
      where: { id: guild.id },
      update: { name: guild.name },
      create: { id: guild.id, name: guild.name },
    });
  }

  console.log('Guild data synchronized');
};

export default handler;

Additional Parameters

CommandKit passes extra parameters after the Discord.js event parameters:
src/app/events/messageCreate/command-handler.ts
import type { EventHandler } from 'commandkit';

const handler: EventHandler<'messageCreate'> = async (
  message,
  client,      // Discord.js Client instance
  commandkit   // CommandKit instance
) => {
  // Access CommandKit features
  const commands = commandkit.commands;
  
  console.log(`Loaded commands: ${commands.size}`);
};

export default handler;

Welcome Messages Example

Send messages when users join:
src/app/events/guildMemberAdd/welcome.ts
import type { EventHandler } from 'commandkit';
import { EmbedBuilder } from 'discord.js';

const handler: EventHandler<'guildMemberAdd'> = async (member) => {
  const welcomeChannel = member.guild.channels.cache.find(
    ch => ch.name === 'welcome'
  );

  if (!welcomeChannel?.isTextBased()) return;

  const embed = new EmbedBuilder()
    .setTitle('Welcome!')
    .setDescription(`Welcome to the server, ${member.user}!`)
    .setThumbnail(member.user.displayAvatarURL())
    .addFields(
      { name: 'Member Count', value: member.guild.memberCount.toString(), inline: true },
      { name: 'Account Created', value: `<t:${Math.floor(member.user.createdTimestamp / 1000)}:R>`, inline: true },
    )
    .setColor('#5865F2')
    .setTimestamp();

  await welcomeChannel.send({ embeds: [embed] });
};

export default handler;

Voice State Tracking

Track voice channel activity:
src/app/events/voiceStateUpdate/track-voice.ts
import type { EventHandler } from 'commandkit';

const handler: EventHandler<'voiceStateUpdate'> = async (oldState, newState) => {
  const member = newState.member;
  if (!member) return;

  // User joined a voice channel
  if (!oldState.channel && newState.channel) {
    console.log(`${member.user.username} joined ${newState.channel.name}`);
  }
  
  // User left a voice channel
  else if (oldState.channel && !newState.channel) {
    console.log(`${member.user.username} left ${oldState.channel.name}`);
  }
  
  // User switched channels
  else if (oldState.channel && newState.channel && oldState.channel.id !== newState.channel.id) {
    console.log(`${member.user.username} moved from ${oldState.channel.name} to ${newState.channel.name}`);
  }
  
  // User muted/unmuted
  if (oldState.selfMute !== newState.selfMute) {
    console.log(`${member.user.username} ${newState.selfMute ? 'muted' : 'unmuted'} themselves`);
  }
  
  // User deafened/undeafened
  if (oldState.selfDeaf !== newState.selfDeaf) {
    console.log(`${member.user.username} ${newState.selfDeaf ? 'deafened' : 'undeafened'} themselves`);
  }
};

export default handler;

Reaction Roles

Implement reaction roles:
src/app/events/messageReactionAdd/reaction-roles.ts
import type { EventHandler } from 'commandkit';

const REACTION_ROLES = new Map([
  ['👾', '1234567890123456789'], // Gamer role
  ['🎨', '1234567890123456790'], // Artist role
  ['🎵', '1234567890123456791'], // Musician role
]);

const REACTION_ROLE_MESSAGE_ID = '1234567890123456792';

const handler: EventHandler<'messageReactionAdd'> = async (reaction, user) => {
  // Ignore bot reactions
  if (user.bot) return;

  // Fetch partial data if needed
  if (reaction.partial) {
    try {
      await reaction.fetch();
    } catch (error) {
      console.error('Error fetching reaction:', error);
      return;
    }
  }

  // Check if it's the reaction role message
  if (reaction.message.id !== REACTION_ROLE_MESSAGE_ID) return;

  // Get the role ID for this emoji
  const roleId = REACTION_ROLES.get(reaction.emoji.name!);
  if (!roleId) return;

  // Add the role
  const member = await reaction.message.guild?.members.fetch(user.id);
  if (!member) return;

  try {
    await member.roles.add(roleId);
    console.log(`Added role to ${user.username}`);
  } catch (error) {
    console.error('Error adding role:', error);
  }
};

export default handler;

Custom Events

Create and emit custom events:
1

Create custom event directory

Use parentheses to create a custom event namespace:
mkdir -p src/app/events/(leveling)/levelUp
2

Create event handler

src/app/events/(leveling)/levelUp/notify.ts
import type { Message } from 'discord.js';

const handler = async (message: Message, newLevel: number) => {
  await message.reply(`🎉 Congratulations! You've reached level ${newLevel}!`);
};

export default handler;
3

Emit the custom event

src/app/events/messageCreate/give-xp.ts
import type { EventHandler } from 'commandkit';
import { getCommandKit } from 'commandkit';

const handler: EventHandler<'messageCreate'> = async (message) => {
  if (message.author.bot) return;

  // XP logic here...
  const didLevelUp = checkLevelUp(message.author.id);
  
  if (didLevelUp) {
    const commandkit = getCommandKit(true);
    const newLevel = getLevel(message.author.id);
    
    // Emit custom event
    commandkit.events
      .to('leveling')
      .emit('levelUp', message, newLevel);
  }
};

export default handler;

Error Handling

Always handle errors in event handlers:
src/app/events/messageCreate/risky-operation.ts
import type { EventHandler } from 'commandkit';
import { Logger } from 'commandkit/logger';

const handler: EventHandler<'messageCreate'> = async (message) => {
  try {
    // Risky operation
    await performRiskyOperation(message);
  } catch (error) {
    Logger.error('Error in messageCreate handler:', error);
    // Don't let one handler failure stop others from running
  }
};

export default handler;

async function performRiskyOperation(message: any) {
  // Implementation
}

Performance Tips

1

Use early returns

Exit early when conditions aren’t met:
const handler: EventHandler<'messageCreate'> = async (message) => {
  if (message.author.bot) return;
  if (!message.guild) return;
  if (message.content.length < 3) return;
  
  // Main logic here
};
2

Cache frequently accessed data

Reduce API calls by caching:
const prefixCache = new Map<string, string>();

const handler: EventHandler<'messageCreate'> = async (message) => {
  let prefix = prefixCache.get(message.guildId!);
  
  if (!prefix) {
    prefix = await fetchPrefix(message.guildId!);
    prefixCache.set(message.guildId!, prefix);
  }
  
  // Use cached prefix
};
3

Use event partials wisely

Only fetch partials when needed:
const handler: EventHandler<'messageUpdate'> = async (oldMessage, newMessage) => {
  // Only fetch if we need the full data
  if (newMessage.partial) {
    try {
      await newMessage.fetch();
    } catch (error) {
      // Handle error or return early
      return;
    }
  }
  
  // Use full message data
};

Best Practices

  • Keep handlers focused: Each handler file should do one thing well
  • Handle errors: Never let errors crash your bot
  • Validate data: Check for nulls and undefined values
  • Use type safety: Leverage TypeScript’s EventHandler type for autocomplete
  • Test thoroughly: Events fire frequently - ensure your handlers are efficient
  • Log important actions: Use CommandKit’s Logger for consistent logging

Build docs developers (and LLMs) love