Throwing
Refuse from a handler by throwing a Notice, so the user gets a reason rather than silence. Covers what each kind of throw produces, the fields on a Notice, and what render receives.
A handler refuses by throwing. seedcord catches the throw at one boundary and sends the reply through the handler, so the class you throw sets what the reply says. A gate throws into the same boundary, and its refusal reaches the user the same way.
Say your ban handler checks for an open case in a helper. Returning a flag means execute() has to check it, and every caller of every helper that can refuse has to check one too. Throw from the helper and the handler stops at the line that found the problem.
You write a refusal as a Notice subclass.
import {
BuilderComponent,
Notice,
SlashHandler,
SlashRoute
} from '@seedcord/gateway';
import type { ReplyResponse } from '@seedcord/gateway';
class OpenCaseCard extends BuilderComponent<'container'> {
constructor() {
super('container');
this.instance.addTextDisplayComponents((text) =>
text.setContent(
'### Cannot ban\nThat user already has an open case.'
)
);
}
}
class OpenCaseExists extends Notice {
constructor(id: string) {
super(`user ${id} has an open case`);
}
public render(): ReplyResponse {
return { components: [new OpenCaseCard().component] };
}
}
@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
public async execute(): Promise<void> {
const target = this.options.getUser('target');
await this.assertNoOpenCase(target.id);
await this.reply(`Banned ${target.username}.`);
}
private async assertNoOpenCase(id: string): Promise<void> {
const open = await cases.countOpen(id);
if (open > 0) throw new OpenCaseExists(id);
}
}Your logs and your fault reports read the string you pass to super(). The user never sees it, so the id goes there. render() builds the reply and returns a ReplyResponse, the same { components } shape that this.reply() takes.
assertNoOpenCase refuses on its own. execute() calls it and continues, since a refusal never comes back as a return value.
super() takes a second argument too, for the error you caught.
class ProfileUnavailable extends Notice {
constructor(cause: unknown) {
super('the profile lookup failed', { cause });
this.report = true;
}
}{ cause } keeps that error on the throw. With report set to true there, the fault report prints its stack.
What each throw produces
Reported means seedcord logs the throw and puts it on the bus, where a webhook reporter can send it to a Discord channel. Faults explains the bottom three rows.
The fields on a Notice
constructor(id: string) {
super(`user ${id} has an open case`);
this.report = true;
this.ephemeral = false;
this.summary = 'the target already has an open case';
}
| field | default | what it changes |
|---|---|---|
report | false | true logs the throw with a uuid and publishes it to the handledException bus key |
ephemeral | true | false shows the reply to everyone in the channel |
summary | unset | a one-line reason the or gate lists when every gate inside it refuses |
Only the or gate reads summary. Combining gates shows where it appears.
Warning
Throwing a custom Notice for a real bug shows its card and produces nothing else, because report starts false. Throw a Fault when you need the error in your logs or set report = true on the Notice.
What render receives
render() takes a RenderContext. Leave the parameter off when you don't read any of its fields, as the first sample does.
| field | holds |
|---|---|
ctx.uuid | the id seedcord logs and reports for this throw |
ctx.developerUsername | notifications.developerUsername from your bot config, set on Faults. undefined when you have not set it |
ctx.dispatch | the dispatch context the handler and its gates read |
public render(ctx: RenderContext): ReplyResponse {
const contact = ctx.developerUsername ?? 'the developer';
return {
components: [new TraceCard(ctx.uuid, contact).component]
};
}
LookupFailed builds its TraceCard inside render(). seedcord calls render() each time it shows the refusal, so every reply gets new builders with your bot color read at that moment.