Error handling
Introduction
Section titled “Introduction”Things will go wrong — and believe me, they will. A database query fails, an API times out, a permission check encounters an unexpected state. When they do, Warden routes errors through a structured handler system rather than letting them crash silently.
Error handlers are classes with decorators, just like commands and events. You write them once, and the framework takes care of catching exceptions and routing them to the right handler.
How errors flow
Section titled “How errors flow”When any handler throws during execute(), the framework catches the error and looks for an appropriate error handler:
- First, it checks for a typed error handler matching the handler type (e.g., one registered for
commanderrors) - If no typed handler is found, it falls back to a catch-all handler
- If no custom handlers exist at all, the framework default kicks in — it logs the error and replies with a generic message for interaction-based handlers
This means your bot never crashes from an unhandled error in a command or event. Even without any custom error handlers, the framework has your back.
Typed error handlers
Section titled “Typed error handlers”For more control, you can create error handlers that are specific to a handler type. In a moderation bot, you probably want command errors to notify the user and log to the mod channel:
import {errorHandler, ErrorHandler, ErrorContext, Embed} from '@warden/core';
@errorHandler({type: 'command'})export default class CommandErrorHandler extends ErrorHandler { constructor(private logger: Logger, private modLog: ModLogService) {}
public async handle(error: Error, context: ErrorContext): Promise<void> { this.logger.error(`Command /${context.name} failed`, error);
if (context.interaction) { await reply(context.interaction, { content: `Something went wrong while running /${context.name}. The incident has been logged.`, ephemeral: true, }); }
await this.modLog.send(Embed.error(`Command error: /${context.name}`) .addFields({name: 'Error', value: error.message}) .setTimestamp() ); }}The available types cover every handler kind in the framework:
| Type | Catches errors from |
|---|---|
command | Slash commands |
context | Context menu commands |
event | Event handlers |
button | Button interactions |
modal | Modal submissions |
menu | Select menu interactions |
job | Scheduled jobs |
The ErrorContext object gives you everything you need to understand what went wrong:
interface ErrorContext { type: 'command' | 'context' | 'event' | 'button' | 'modal' | 'menu' | 'job'; name: string; // the handler's name interaction?: Interaction; // present for interaction-based handlers args?: unknown[]; // raw arguments (for events)}Of course, a moderation bot benefits from granular error handling. You might want command errors to reply to the user, event errors to just log, and job errors to alert the mod team:
@errorHandler({type: 'event'})export default class EventErrorHandler extends ErrorHandler { constructor(private logger: Logger) {}
public async handle(error: Error, context: ErrorContext): Promise<void> { // events have no interaction to reply to — just log this.logger.error(`Event error: ${context.name}`, error); }}
@errorHandler({type: 'job'})export default class JobErrorHandler extends ErrorHandler { constructor(private logger: Logger, private modLog: ModLogService) {}
public async handle(error: Error, context: ErrorContext): Promise<void> { this.logger.error(`Job error: ${context.name}`, error); await this.modLog.send(Embed.error(`Scheduled job "${context.name}" failed`) .addFields({name: 'Error', value: error.message}) .setTimestamp() ); }}Catch-all handlers
Section titled “Catch-all handlers”If you’d prefer a single handler for everything, simply omit the type parameter. This catch-all handler will receive errors from any handler type that doesn’t have its own typed handler:
@errorHandler()export default class FallbackErrorHandler extends ErrorHandler { constructor(private logger: Logger) {}
public async handle(error: Error, context: ErrorContext): Promise<void> { this.logger.error(`Unhandled error in ${context.type}: ${context.name}`, error); }}Authorization errors
Section titled “Authorization errors”You might expect AuthorizationError to receive special treatment — and in some frameworks it does. In Warden, it’s just another error. It flows through the exact same handler chain as any other exception.
The framework’s built-in default handler does recognize AuthorizationError and replies with a localized permission message. But if you’ve defined your own error handler, you’re in full control:
@errorHandler({type: 'command'})export default class CommandErrorHandler extends ErrorHandler { public async handle(error: Error, context: ErrorContext): Promise<void> { if (error instanceof AuthorizationError) { if (context.interaction) { await reply(context.interaction, { embeds: [Embed.error('You do not have permission to use this command.')], ephemeral: true, }); } return; }
// handle other errors... }}No magic, no special cases. You decide how authorization failures are communicated.
Plugin integration
Section titled “Plugin integration”Error handlers work beautifully with Warden’s plugin ecosystem.
If you’re using @warden/audit, you can track both successes and failures by adding trackResult: true:
@command({name: 'ban', description: 'Ban a user'})@permissions({user: ['BanMembers'], bot: ['BanMembers']})@audit({action: 'moderation.ban', trackTarget: 'user', trackResult: true})export default class BanCommand implements Command { ... }When @warden/sentry is available (planned for v1.1), it hooks into the error handler system automatically. Simply register the plugin and errors are captured with full context — command name, guild, user, interaction data — with zero additional code.
Testing error handlers
Section titled “Testing error handlers”You can verify your error handlers work as expected using TestBot:
import {TestBot, FakeInteraction} from '@warden/core/testing';
const bot = TestBot.create().discover(d => d.load('./src/**/*.ts'));
const interaction = FakeInteraction.chatInput({commandName: 'ban'});await bot.execute(interaction);
expect(interaction.replied).toBe(true);expect(interaction.lastReply).toContain('Something went wrong');For more on testing, see the Testing guide.