Registering and Handling a Discord.js v14 Guild Slash Command
Add a Discord.js v14 slash command to a test guild: define it with SlashCommandBuilder, register via REST, handle interactionCreate, and verify replies.
10 Jan 2026, 17:58 UTC

The problem: commands that never appear or expire before replying
A slash command that does not show up in your server, or one that appears but fails with an Unknown interaction error, usually traces back to registration scope or a missed reply deadline rather than a bug in your handler. This guide defines a single /ping command with SlashCommandBuilder, registers it to one test guild through Discord's REST API, handles the resulting interactionCreate event, and verifies the reply path.
Desired outcome
By the end, your bot will expose a /ping command visible only in a designated test guild. Invoking it will produce a reply containing a simple latency measurement. The command definition and its handler will live in the same module so that registration and execution cannot drift apart.
Prerequisites and version check
You need a Discord bot application invited to your test server with the applications.commands scope. The bot token must be available as an environment variable; never hard-code it or commit it to source control. You also need the guild ID of your test server.
Because v13 and v14 differ in builder names, intent names, and REST route helpers, confirm the installed major version before writing code. Run the following in your terminal from the project root:
npm ls discord.js @discordjs/rest discord-api-types
node -e "console.log(require('discord.js').version)"
If the printed version does not start with 14, upgrade or adjust the APIs used below. Gateway intents are the event categories your bot subscribes to when connecting; slash command delivery requires only the Guilds intent. Do not enable privileged intents such as MessageContent unless another feature needs them.
Define the command module
Create a file that exports both the command payload and an execution function. Keeping them together means the object you register is the same object you execute.
// commands/ping.js
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check bot latency')
.toJSON(),
async execute(interaction) {
const latency = Date.now() - interaction.createdTimestamp;
await interaction.reply(`Pong! Gateway latency: ${latency}ms`);
},
};
SlashCommandBuilder produces a structured payload that Discord validates on registration. The toJSON() call converts the builder into the plain object expected by the REST route. Command and option names must follow Discord's naming rules (lowercase, no spaces, limited character set), or registration will fail with a validation error.
Register the command to a test guild
Discord exposes a REST API—an HTTP interface for creating and updating resources—that you call with a token. Guild-scoped registration updates immediately, which makes it the right choice during development. Global registration can lag, so a missing command right after registration is not proof of failure. Run this script from your project root with your bot token in the environment:
// register.js
const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
commands.push(command.data);
}
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log('Started refreshing application (/) commands.');
await rest.put(
Routes.applicationGuildCommands(
process.env.CLIENT_ID,
process.env.GUILD_ID
),
{ body: commands },
);
console.log('Successfully reloaded application (/) commands.');
} catch (error) {
console.error(error);
}
})();
Replace CLIENT_ID with your application's ID and GUILD_ID with your test server's ID. The script requires no special channel permissions beyond the bot's existing applications.commands scope. If registration returns a 400 validation error, check the command name and option names against Discord's naming rules. If it returns 401 or 403, verify that the token is correct and has not been rotated.
Handle the interaction
In your main bot file, enable the Guilds intent and listen for interactionCreate. Discord expects an initial response within three seconds; if your handler takes longer, call deferReply() first and then editReply() when work finishes. An ephemeral reply is visible only to the user who invoked the command.
// index.js
const { Client, Collection, GatewayIntentBits, Events } = require('discord.js');
const fs = require('node:fs');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.commands = new Collection();
for (const file of fs.readdirSync('./commands').filter(f => f.endsWith('.js'))) {
const command = require(`./commands/${file}`);
client.commands.set(command.data.name, command);
}
client.once(Events.ClientReady, readyClient => {
console.log(`Logged in as ${readyClient.user.tag}`);
});
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing ${interaction.commandName} for interaction ${interaction.id}`, error);
const method = interaction.replied || interaction.deferred ? 'editReply' : 'reply';
await interaction[method]({ content: 'There was an error while executing this command!', ephemeral: true });
}
});
client.login(process.env.DISCORD_TOKEN);
The Collection acts as a keyed map so that registration and execution stay synchronized. If a command file is missing from the collection, the handler silently ignores the interaction rather than crashing.
Expected checks
After running the registration script and starting the bot, perform these checks:
- The
/pingentry appears in your test guild's command picker. - Invoking
/pingtriggers theinteractionCreatehandler and the bot replies with a latency string. - The reply is visible to the channel (or ephemeral, if you set that flag) as intended.
- Re-running the registration script does not produce duplicate local handler errors, because the
Collectionis rebuilt from the same files each time.
For deeper local debugging, temporarily log interaction.id, interaction.commandName, and interaction.options inside the handler, invoke the command, then remove the logs and invoke it once more to confirm normal behavior.
Recovery and rollback options
If the command does not appear, confirm you registered to the correct guild ID and that the bot is a member of that guild. If the handler times out, the interaction likely exceeded Discord's three-second initial response window; call deferReply() before the slow work and editReply() afterward.
An Unknown interaction error means the interaction token expired before your code responded; defer earlier or move heavy work after the initial reply. A Missing Access or Missing Permissions error indicates the bot lacks the ability to post in that channel; adjust the bot's role or channel permissions.
Because registration changes state, removing the command is the rollback path: run the registration script again with an empty commands array to clear guild-scoped commands, then re-add the command file. If a token is ever exposed, rotate it in the Developer Portal immediately; environment variables alone do not revoke an already-leaked secret.
Limitations and safety notes
Guild registration propagates faster than global registration, but it is still not instantaneous in every case. This guide covers only chat input commands; user and message context commands use different builder methods. The example assumes discord.js v14 and a Node runtime compatible with it; verify your installed version before adapting the code. Finally, never commit .env files, and treat any logged interaction data as sensitive during development.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.