Skip to content

Tuning error behavior

Set what your bot does with an error through the errors block on your config. Covers replacing the card an unknown fault shows, the two ignore lists, and catching seedcord's own errors by code.

Seedcord takes an errors block on its config. Every key in it is optional.

src/bot.ts
import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits, RESTJSONErrorCodes } from 'discord.js';

export const seedcord = new Seedcord({
    bot,
    subscribers: { path: null },
    errors: {
        errorStack: true,
        logSilences: false,
        defaultError: BrandedFault,
        catchProcessErrors: false,
        ignoreApiCodes: [RESTJSONErrorCodes.UnknownMessage],
        ignoreEventApiCodes: [RESTJSONErrorCodes.UnknownMember]
    }
});
keydefaultwhat it changes
errorStackfalsetrue prints the whole stack in your terminal for an unknown fault
logSilencestruefalse drops the debug line written for a Silence reason
defaultErrorthe built-in cardthe class seedcord renders for an unknown fault
catchProcessErrorstruefalse leaves the unhandledRejection and uncaughtException listeners to you
ignoreApiCodes[]Discord api codes the interaction path swallows
ignoreEventApiCodes[]Discord api codes the event path swallows

Faults covers what catchProcessErrors turns off.

Replacing the card

Every unknown fault shows the same generic card, asking the user to contact the person set in notifications.developerUsername. Replace it when your bot needs its own wording, or an action the user can take from the card.

defaultError takes a class that seedcord constructs with the fault's uuid and then renders. Subclass Notice and build your own card.

src/stops/BrandedFault.ts
class BrandedCard extends BuilderComponent<'container'> {
    constructor(uuid: string) {
        super('container');

        this.instance.addTextDisplayComponents((text) =>
            text.setContent(`### Something broke\nCode \`${uuid}\``)
        );
    }
}

export class BrandedFault extends Notice {
    public constructor(private readonly uuid: string) {
        super(`unknown fault ${uuid}`);
    }

    public render(): ReplyResponse {
        return {
            components: [new BrandedCard(this.uuid).component]
        };
    }
}

Tip

Put a button on that card. The user can then file the fault themselves. The customId carries the uuid, so the click arrives with the code on it.

src/handlers/ReportFault.ts
import { ButtonBuilder } from '@discordjs/builders';
import {
    ButtonHandler,
    ButtonRoute,
    CustomId,
    RowComponent
} from '@seedcord/gateway';
import { ButtonStyle } from 'discord.js';

export const ReportFault = new CustomId('report').uuid('uuid');

export class ReportRow extends RowComponent<'button'> {
    constructor(uuid: string) {
        super('button');

        this.instance.addComponents(
            new ButtonBuilder()
                .setCustomId(ReportFault.encode({ uuid }))
                .setLabel('Report this')
                .setStyle(ButtonStyle.Secondary)
        );
    }
}

@ButtonRoute(ReportFault)
export class OpenTicket extends ButtonHandler<
    [typeof ReportFault]
> {
    public async execute(): Promise<void> {
        await tickets.open(this.params.uuid);

        await this.reply('Thanks. Someone will take a look.');
    }
}

Add the row to BrandedCard through addActionRowComponents, passing it new ReportRow(uuid).component. Custom ids explains how a customId carries a value like that uuid.

The two ignore lists

Both ignore lists start empty, so every api code your own work throws reports as a fault. A real bug surfaces that way, like a double ack from a misplaced defer(). Add a code once you've confirmed it's an expected dead end for your bot. A swallowed code still writes a debug line.

Sending the fault card itself is a separate case. seedcord always drops a 10062, 40060, or 10008 that comes back from that send, whatever these lists hold. UnknownInteraction (10062) means the interaction expired. InteractionHasAlreadyBeenAcknowledged (40060) means something already answered it. UnknownMessage (10008) means the message the card would edit is gone.

Gateway only

ignoreEventApiCodes covers the event path, which only a gateway bot has. Discord delivers dead resources on events, like a reaction on a deleted message or a member who just left. Your handler's own fetch can throw on one. Until you add the code, each throw reports once a minute per handler. UnknownMessage is 10008, UnknownMember is 10007, and UnknownUser is 10013.

Catching a framework error

When you wrap a seedcord call in try, branch on the error's code. The message wording can change in any release. A change to a code is always marked as breaking, so the changelog tells you when to update your check. Your transport package re-exports what you need from @seedcord/errors.

Every error the framework throws carries one of those codes. isSeedcordError narrows a caught value to a seedcord error.

if (isSeedcordError(caught)) {
    logger.warn(`seedcord threw ${caught.code}`);
}

if (isSeedcordError(caught, 'SeedcordRangeError')) {
    const ranged = caught;
const ranged: SeedcordRangeError<SeedcordErrorCode.ConfigEmojiUnresolved> | SeedcordRangeError<SeedcordErrorCode.ConfigWebhookUrlInvalid> | SeedcordRangeError<SeedcordErrorCode.ConfigWebhookNotFound> | SeedcordRangeError<SeedcordErrorCode.ConfigManifestNotGenerated> | SeedcordRangeError<SeedcordErrorCode.ConfigMissingEnv> | SeedcordRangeError<SeedcordErrorCode.ConfigInvalidEnv> | SeedcordRangeError<SeedcordErrorCode.MissingGuildsIntent> | ... 120 more ... | SeedcordRangeError<...>
} const taken = SeedcordErrorCode.CorePluginKeyExists; if (isSeedcordError(caught, undefined, taken)) { const exact = caught.code;
const exact: SeedcordErrorCode.CorePluginKeyExists
} const priority = SeedcordErrorCode.DecoratorInvalidMiddlewarePriority; if (isSeedcordError(caught, 'SeedcordTypeError', priority)) { const both = caught; }

The second argument narrows by class, one of 'SeedcordError', 'SeedcordTypeError', 'SeedcordRangeError', or 'SeedcordAggregateError'. The third matches one exact code. Pass undefined as the second argument when the class doesn't matter, or pass both when you need the class and the code to match together.