Spraxium logoSpraxium

Guards, Permissions, and Error Handling

Protect slash command handlers with guards, Discord permissions, and structured error handling. Learn the three-level guard pipeline, permission bitfields, and how SpraxiumException flows from handler to Discord reply.

Permission model overview

Slash command access control operates on two layers. The first layer is defaultMemberPermissions, a Discord-level flag set during command registration that controls which users see the command in the picker. The second layer is the @UseGuards pipeline, a server-side check that runs in the bot process before the handler executes. Both layers work together: Discord filters the client-side UI, and guards enforce rules that Discord cannot check, like custom role hierarchies, database-backed allowlists, or time-based restrictions.

Neither layer alone is sufficient for security. defaultMemberPermissions is a UI hint that server administrators can override through Discord channel permission overrides. Guards run in the bot but require the interaction to reach the bot in the first place. Using both layers ensures that unauthorized users cannot see the command and that even if they somehow invoke it, the guard pipeline blocks execution before any business logic runs.

The exception handler sits at the end of the pipeline. When a guard denies the interaction or a handler throws, the framework catches the error, normalizes it into a SpraxiumException, resolves the appropriate reply layout, and sends a Discord message. This means handlers never need raw try/catch blocks for expected error scenarios.

Setting defaultMemberPermissions

The defaultMemberPermissions property in the @SlashCommand config accepts a permission bitfield that determines which users can see the command by default. Use the PermissionFlagsBits enum from discord.js for type-safe bitfield construction. The most common patterns are requiring a single permission, combining multiple permissions with bitwise OR, and setting null to allow everyone.

PatternValueEffect
Single permissionPermissionFlagsBits.BanMembers

Only users with the Ban Members permission see the command.

Multiple permissions

PermissionFlagsBits.BanMembers plus PermissionFlagsBits.KickMembers

Users need BOTH permissions to see the command (bitwise OR combines the flags).

Administrator onlyPermissionFlagsBits.Administrator

Only users with the Administrator permission see the command.

Everyonenull, or omit the property

All users see the command in the picker regardless of their permissions.

Bigint literal0x20n

Equivalent to PermissionFlagsBits.ManageGuild. Useful when importing the enum is not desirable.

commands/ban.command.ts
import { SlashCommand, SlashOption } from '@spraxium/common';
import { PermissionFlagsBits } from 'discord.js';

@SlashCommand({
  name: 'ban',
  description: 'Ban a member from the server.',
  defaultMemberPermissions: PermissionFlagsBits.BanMembers,
})
export class BanCommand {
  @SlashOption.User('target', {
    description: 'The member to ban',
    required: true,
  })
  @SlashOption.String('reason', {
    description: 'Reason for the ban',
    required: false,
    maxLength: 512,
  })
  @SlashOption.Integer('days', {
    description: 'Number of days of messages to delete',
    required: false,
    min: 0,
    max: 7,
  })
  build() {}
}

The guard pipeline

Guards are classes that implement the SpraxiumGuard interface with a single canActivate(ctx) method. The method receives a SpraxiumExecutionContext wrapping the interaction and command name, and returns a boolean (or a promise that resolves to a boolean). Returning true allows the interaction to proceed to the handler. Returning false stops the pipeline and triggers a GuardDeniedException.

The GuardExecutor evaluates guards in a strict order: global guards first, then class-level guards, then method-level guards. It stops at the first false result. This means global guards act as baseline checks that every command must pass, class-level guards filter specific handler classes, and method-level guards provide the finest control.

Guard levelHow to registerScope
Global

Registered in the GuardRegistry during module loading.

Runs before every command handler in the application.

Class-level@UseGuards(Guard) on the handler class.Runs before every method on that handler class.
Method-level@UseGuards(Guard) on the handle method.Runs before that specific handler method only.

Writing a custom guard

A guard class is decorated with @Injectable() (implicit through the DI system) and implements a canActivate method. The execution context provides getInteraction() for the raw Discord interaction and getCommandName() for the matched command name. From the interaction, you can access the user, guild, channel, member, and any other Discord data needed for the authorization decision.

guards/moderator.guard.ts
import type { ExecutionContext, SpraxiumGuard } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';

export class ModeratorGuard implements SpraxiumGuard {
  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const interaction = ctx.getInteraction() as ChatInputCommandInteraction;
    if (!interaction.memberPermissions) return false;
    return interaction.memberPermissions.has('ModerateMembers');
  }
}

The guard checks whether the member has the ModerateMembers permission. If the interaction is in DMs (no memberPermissions), the guard returns false. If the member has the permission, the guard returns true and the handler proceeds. This is a server-side check that cannot be bypassed by Discord permission overrides.

Applying guards to handlers

Apply guards to handler classes using the @UseGuards decorator. You can pass one or more guard classes, and they are evaluated in the order they appear. Guards can also be applied at the method level by decorating the handle method directly.

import { Ctx, SlashCommandHandler, SlashOpt, UseGuards } from '@spraxium/common';
import type { ChatInputCommandInteraction, User } from 'discord.js';
import { ModeratorGuard } from '../../guards/moderator.guard';
import { BanCommand } from '../commands/ban.command';

@UseGuards(ModeratorGuard)
@SlashCommandHandler(BanCommand)
export class BanHandler {
  async handle(
    @Ctx() interaction: ChatInputCommandInteraction,
    @SlashOpt('target') target: User,
    @SlashOpt('reason') reason: string | null,
    @SlashOpt('days') days: number | null,
  ): Promise<void> {
    const reasonText = reason ?? 'No reason provided';
    await interaction.reply(`${target} has been banned. Reason: ${reasonText}`);
  }
}

Guard options with withOptions

Some guards accept configuration options that change their behavior per handler. The framework supports passing typed options to guards using the withOptions helper. Guard option fields are declared with the @GuardOption decorator on the guard class, specifying default values and whether the option is required.

guards/cooldown.guard.ts
import {
  GuardOption,
  type ExecutionContext,
  type SpraxiumGuard,
} from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';

export class CooldownGuard implements SpraxiumGuard {
  @GuardOption({ default: 5000, required: false })
  duration!: number;

  private readonly timestamps = new Map<string, number>();

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const interaction = ctx.getInteraction() as ChatInputCommandInteraction;
    const key = `${interaction.user.id}:${ctx.getCommandName()}`;
    const now = Date.now();
    const last = this.timestamps.get(key) ?? 0;

    if (now - last < this.duration) return false;

    this.timestamps.set(key, now);
    return true;
  }
}

Error replies are handled automatically

When a guard returns false or a handler throws, the framework catches the error and sends a reply to Discord automatically. Throw a SpraxiumException subclass with a meaningful message and let the exception handler resolve the correct reply layout.

Exception handler configuration

The exception handler behavior is configured through the exceptions property in the Spraxium configuration. The key options are logUnhandled (whether to log non-framework errors to the console) and layouts (a map of exception classes to layout classes for custom error replies).

spraxium.config.ts
import { defineConfig } from '@spraxium/core';

export default defineConfig({
  exceptions: {
    logUnhandled: true,
    layouts: [],
  },
});

When logUnhandled is true (the default), any thrown value that is not a SpraxiumException subclass is logged with its message and stack trace before being wrapped in an InternalException. Setting it to false suppresses the console output but still sends the error reply to Discord.

Combining permissions and guards effectively

The recommended pattern for slash command access control uses defaultMemberPermissions as the first filter and @UseGuards as the second. The permission bitfield reduces the number of unauthorized interactions that reach the bot, and the guard validates conditions that Discord cannot check.

import { SlashCommand, SlashOption } from '@spraxium/common';
import { PermissionFlagsBits } from 'discord.js';

@SlashCommand({
  name: 'ban',
  description: 'Ban a member from the server.',
  defaultMemberPermissions: PermissionFlagsBits.BanMembers,
})
export class BanCommand {
  @SlashOption.User('target', { description: 'The member to ban', required: true })
  @SlashOption.String('reason', { description: 'Reason for the ban', required: false })
  build() {}
}

Design checklist for guards and error handling

Set defaultMemberPermissions on sensitive commands

Every command that performs a destructive or privileged action should set defaultMemberPermissions. This prevents unauthorized users from seeing the command in the picker, reducing the surface area for accidental or malicious invocations. Use PermissionFlagsBits for type safety.

Use guards for business-level authorization

Guards handle conditions that Discord permissions cannot express: database role hierarchies, feature flags, time-based restrictions, guild-specific allowlists. Write focused guard classes that check one condition each, and compose them with multiple @UseGuards entries.

Let the exception handler manage error replies

Do not catch errors in handlers just to send error messages. Throw SpraxiumException subclasses with meaningful codes and messages, and let the exception handler resolve the correct layout and send the reply. This keeps handlers clean and ensures consistent error formatting across the bot.

Configure custom layouts for expected errors

Register custom layout classes for exception types that your bot handles frequently. A layout class can render a themed embed with your bot branding, specific colors for different error types, and actionable instructions for the user. This turns error responses into a polished part of the user experience.

Enable logUnhandled in development

Keep logUnhandled enabled during development so unexpected errors appear in the console with full stack traces. In production, consider whether you want console logging or if errors should flow to an external logging service through a custom exception handler.

Execution context API reference

The SpraxiumExecutionContext class wraps the interaction and provides a consistent interface for guards and exception handlers.

MethodReturn typeDescription
getInteraction()BaseInteraction or Message

Returns the raw Discord interaction or message that triggered the command.

getCommandName()string

Returns the matched command name as registered in the command definition.

Guards should cast the return value of getInteraction() to the expected type (like ChatInputCommandInteraction) before accessing type-specific properties like memberPermissions or guild. The execution context does not enforce the interaction type to maintain flexibility across different command systems (slash, prefix, message component).