Slash Commands
Overview
Slash commands are registered on a bot.Router and dispatched as *bot.InteractionContext. The source-backed example covers string, integer, boolean, and user options; global and per-command middleware; guild-only and permission checks; public, ephemeral, embed, deferred, and follow-up responses; and automatic registration after READY.
Prerequisites
- Go
1.26.4or newer. DISCORD_TOKENset to a bot token.- An installed application in a test guild.
GuildsandGuildMessagesenabled for the source example. Slash-only applications may be able to use onlyGuilds, depending on the commands they implement.- For
/kick, the invoking member must haveKickMembers, and the bot must have the corresponding channel and guild permission.
Architecture
The router owns command definitions, options, middleware, validation, and command synchronization. Discord sends an interaction after the user invokes a command. InteractionContext parses options and owns the initial callback, deferral, follow-up, embed, and ephemeral response methods. Middleware runs before the command handler and can reject a request without executing business logic.
Quick Start
The commands in this guide are source-backed:
export DISCORD_TOKEN='replace-with-a-bot-token'
go run ./docs/examples/code/slash_commandsTry /hello, /userinfo, /kick, or /serverinfo after the READY log appears. Global registration can take time to propagate.
Complete Runnable Example
examples/slash_commands/main.go is the complete runnable program. It includes package main, imports, token validation, all option definitions, middleware, handlers, and the bot lifecycle. The command above runs that exact file through the module.
Explanation
router.Command takes a lower-case name, a non-empty description, a handler, and zero or more interactions.ApplicationCommandOption values. The handler uses GetStringOption, GetIntOption, GetBoolOption, and GetUserID rather than decoding raw interaction JSON.
The example uses bot.GuildOnly() and bot.RequirePermissions(permissions.KickMembers) as per-command middleware. ctx.Defer() acknowledges a slow interaction, after which ctx.Followup sends the result. A handler must not attempt a second initial response after Reply or Defer; the context returns bot.ErrInteractionAlreadyResponded for that mistake.
Basic Usage
- Register a command with
router.Command. - Read optional strings as empty values and apply an application default.
- Use
Replyfor a public initial response andReplyEphemeralfor a private one. - Attach the router with
bot.WithRouter(router). - Validate a static registry with
router.Validate()or useCommandE/MustCommandduring startup.
Intermediate Usage
- Add logging or authorization once with
router.Use. - Chain per-command middleware with
Command.Use. - Use
GuildOnlyfor commands that dereferencectx.GuildIDorctx.Member. - Use
Deferbefore REST, database, or network work and report the result withFollowuporFollowupEmbed. - Use a guild-scoped sync option during development to avoid waiting for global propagation.
Advanced Usage
- Use
UserCommand,MessageCommand, andTargetIDfor context-menu actions. - Use
Cooldownfor per-user interaction throttling. - Use
RequireBotPermissionsas well as invoker permission middleware so missing bot permissions become a controlled response. - Use
WithCommandSyncto set global, guild, or disabled synchronization and a finite sync timeout. - Instrument
Bot.Stats()andOnError; command handlers can panic or fail independently of the Gateway.
Common Patterns
- Treat option values as untrusted and validate ranges, lengths, and target relationships.
- Use
ctx.MemberPermissions()and middleware for authorization, not a user-supplied option. - Keep the first response small and edit or follow up after expensive work.
- Make administrative commands guild-only and record an audit reason in REST requests.
- Use a distinct test bot and guild for global command development.
Best Practices
- Keep command descriptions and option descriptions meaningful and within Discord limits.
- Prefer ephemeral error details when revealing permission or lookup information.
- Defer within Discord's initial interaction response window; a normal REST timeout does not extend that deadline.
- Never use
context.Background()for unbounded production REST work; derive a timeout from the operation or service context. - Handle failures from the initial response, deferral, follow-up, and command synchronization separately.
Common Mistakes with wrong/correct examples
Wrong
router.Command("hello", "", func(ctx *bot.InteractionContext) {
go slowLookup()
_ = ctx.Reply("finished")
})Correct
router.Command("hello", "Run a greeting", func(ctx *bot.InteractionContext) {
if err := ctx.Defer(); err != nil {
return
}
// Perform bounded work, then use a follow-up.
_, _ = ctx.Followup("finished")
})Wrong
router.Command("kick", "Kick a member", handler)Correct
router.Command("kick", "Kick a member", handler).
Use(bot.GuildOnly()).
Use(bot.RequirePermissions(permissions.KickMembers)).The corrections are excerpts; the complete source is the linked program.
Expected Result
The router synchronizes the registered commands after READY. /hello reads its options and replies, /userinfo demonstrates a deferral and follow-up, /kick is rejected outside a guild or without permission, and /serverinfo returns an embed for guild invocations.