Skip to content

When a reply throws

Discord tracks what state an interaction is in, and that decides which reply methods still work. Covers the states, the methods each one allows, and three errors a reply call throws.

seedcord checks the method against the interaction's current state before it calls Discord. An illegal call throws without reaching the API, where Discord would reject it with an error that doesn't say which method to use. The message says which method you called, why it can't run, and which method works in that state.

reply() was called when this interaction was already replied to.
Use followUp() for a new message or edit() to rewrite the reply. (route slash:ban)

The error's cause is a second error, such as reply() acknowledged this interaction, whose stack points at the line that answered first. One throw gives you both lines of a double ack, including a first one inside a helper in another file.

The seedcord dev terminal showing SeedcordError 1501 for a second reply() at Ban.ts line 7, then Caused by AckTrace, reply() acknowledged this interaction, at Ban.ts line 6

The top stack ends at Ban.ts:7, the second reply(). The AckTrace under it ends at Ban.ts:6, the reply that went out first. Your terminal prints both stacks when errorStack is on.

The states

statehow you get there
unackednothing sent yet
deferred-replydefer()
deferred-updatedeferUpdate()
repliedreply(), update(), or showModal()

update() and deferUpdate() are defined on component and modal handlers, covered in the Components tab. showModal() is on every command and component handler. A modal handler doesn't have it, since Discord doesn't let a modal open another modal.

Which method each state allows

statelegalthrows
unackedreply, defer, deferUpdate, update, showModal, sendfollowUp, edit, delete
deferred-replyedit, followUp, delete, sendreply, defer, deferUpdate, update, showModal
deferred-updateupdate, edit, followUp, delete, sendreply, defer, deferUpdate, showModal
repliededit, followUp, delete, sendreply, defer, deferUpdate, update, showModal

send() runs from every state, since it picks whichever method the state allows. Follow-ups and edits shows the mapping.

A component that fails to serialize

seedcord calls toJSON() on every component in your reply, and discord.js validates the builder there. Its own throw is a nested shapeshift error, which doesn't say which entry in components failed. seedcord catches it and rewrites it as one line with the class, its index in components, and what went wrong.

src/handlers/Ban.ts
import {
    BuilderComponent,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';

class BanText extends BuilderComponent<'text_display'> {
    constructor() {
        super('text_display');
    }
}

@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
    public async execute(): Promise<void> {
        await this.reply({
            components: [new BanText().component]
        });
    }
}
TextDisplayBuilder at components[0] failed to serialize: need a
string, got nothing. (route slash:ban)

BanText above never calls setContent, so its builder never got a string to send. The detail after the colon reads need <the rule>, got <your value>. A bound uses at most, at least, fewer than, or more than, and a rule carrying two bounds joins them with and. A failure on a named field puts that field and a colon first, as with url: on a file component's link.

The got half describes the value you passed.

what you passedhow it reads
undefinednothing
nullnull
an empty stringan empty string
a string up to 40 charactersthe string, quoted
a longer stringthe character count
an empty arrayan empty list
a longer arraythe item count
a number, bigint, or booleanthe value itself
any other objectits class name

A discord.js throw that never reaches its validator keeps its own message, like "Non-premium buttons must have a label and/or an emoji."

The original throw stays on the error as its cause, so you can still read the nested shapeshift error underneath.

The interaction callback returned no message

reply() and update() read the created message out of Discord's callback response.

The interaction callback for reply() returned no message.
(route slash:ban)

Your handler can't cause this throw. The callback carries that message on every documented path, so this one fires when Discord's response doesn't include it.

update() on a modal a command opened

update() rewrites the message that a component came from. A modal that your command opened does not carry one.

update() was called on a modal opened from a command, which has no
source message.
Use reply() or defer() instead. (route slash:ban)