Skip to main content

Migrating to v1

This guide covers all breaking changes introduced in v1.0.0 and how to update your bot accordingly.

Stoatx​

messageCreate is no longer handled internally​

The framework no longer registers a messageCreate listener automatically. You must wire it up yourself using @On in an event class, or inline with client.on("messageCreate", ...).

Before:

const client = new Client({ prefix: "!" });
await client.initCommands();
await client.login(token);

After:

const client = new Client({ prefix: "!" });
await client.login(token);
// src/events/message.ts
import { Stoat, On } from "stoatx";
import type { Client, Message } from "stoatx";

@Stoat()
export class MessageEvents {
@On("messageCreate")
async onMessage(message: Message, client: Client) {
if (message.author.bot) return;
await client.executeCommand(message);
}
}

initCommands() has been removed​

Command loading now happens automatically inside login(). Remove any initCommands() calls from your startup code.

@Arg and @Option replace inline args/options​

The args and options arrays on @SimpleCommand have been removed. Define your command parameters directly on the method using the @Arg and @Option decorators instead.

Before:

@SimpleCommand({
name: "ban",
args: [{ name: "target", type: "user", required: true }],
options: [
{ name: "reason", type: "string" },
{ name: "deleteDays", type: "number" },
],
})
async ban(ctx: CommandContext<{ reason?: string; deleteDays?: number }, [string]>) {
const targetId = ctx.args[0];
const reason = ctx.options.reason;
}

After:

@SimpleCommand({ name: "ban" })
async ban(
@Arg({ required: true }) target: User,
@Option({ name: "reason" }) reason: string | undefined,
@Option({ name: "deleteDays" }) deleteDays: number | undefined,
ctx: CommandContext
) {
await target.ban();
}

Types are inferred automatically from the TypeScript parameter type — no more manual type: "user" strings. The ctx parameter always goes last and requires no decorator.

fetch support for mention types​

User, BaseChannel, and Role parameters now support fetch: true to hit the API instead of looking up the cache:

@Arg({ required: true, fetch: true }) target: User

If the fetch fails, onValidationError is called with a FetchFailedError.

CommandContext generics removed​

CommandContext<TOptions, TArgs> no longer accepts generics. Since args and options are now typed method parameters, the generics are unnecessary.

Before:

async ban(ctx: CommandContext<BanOptions, BanArgs>) {
const targetId = ctx.args[0];
const reason = ctx.options.reason;
}

After:

async ban(
@Arg({ required: true }) target: User,
@Option({ name: "reason" }) reason: string | undefined,
ctx: CommandContext
) {}

ctx.args and ctx.options no longer exist on CommandContext.

Error handling overhaul​

Validation errors and runtime errors are now handled separately.

onValidationError replaces onError for input validation​

Previously all errors — both runtime and validation — went through onError. Validation errors (missing arguments, wrong types, invalid mentions, failed fetches) now go through onValidationError instead.

Before:

async onError(ctx: CommandContext, error: Error) {
if (error instanceof CommandValidationError) {
await ctx.reply(`⚠️ ${error.message}`);
} else {
await ctx.reply("Something went wrong.");
}
}

After:

async onValidationError(ctx: CommandContext, error: CommandValidationError) {
await ctx.reply(`⚠️ ${error.message}`);
}

async onError(ctx: CommandContext, error: Error) {
console.error(error);
await ctx.reply("Something went wrong. Please try again later.");
}

onError is now exclusively for unhandled runtime errors thrown inside the command body.

New error subclasses​

CommandValidationError is now a base class with specific subclasses for each failure case. Use instanceof in onValidationError for granular handling:

import {
CommandValidationError,
MissingArgumentError,
MissingOptionError,
InvalidTypeError,
InvalidMentionError,
FetchFailedError,
NoServerContextError,
} from "stoatx";

async onValidationError(ctx: CommandContext, error: CommandValidationError) {
if (error instanceof FetchFailedError) {
await ctx.reply(`Couldn't find that ${error.mentionKind}.`);
} else if (error instanceof MissingArgumentError) {
await ctx.reply(`Missing required argument: \`<${error.paramName}>\``);
} else if (error instanceof InvalidMentionError) {
await ctx.reply(`\`${error.rawValue}\` is not a valid ${error.mentionKind}.`);
} else if (error instanceof InvalidTypeError) {
await ctx.reply(`Expected ${error.expected} but got \`${error.received}\`.`);
} else {
await ctx.reply(`⚠️ ${error.message}`);
}
}

CommandValidationError constructor changed​

If you were constructing CommandValidationError directly, the signature has changed.

Before:

new CommandValidationError(optionName, message);

After:

new CommandValidationError(paramName, paramKind, message);
// e.g.
new CommandValidationError("target", "arg", "Custom error message");