Creating a modal after a button click shows an "Unknown Interaction" error

I've created a system which dynamically creates a sequence of embeds, components and select fields for a Setup command. My hope is that I can also add into it an option for creating a Modal, so when I need a text input from the user I can show it in a more elegant way than just "type a message in chat". Whenever I send a modal using this system (clicking a button can generate a Modal element), the modal shows up on Discord, but the bot crashes with an DiscordAPIError[10062]: Unknown interaction error. Researching, I came across this:
Showing a modal must be the first response to an interaction. You cannot defer() or deferUpdate() then show a modal later.
Does this mean that this kind of action is not possible, and that modals are exclusive to being used immediately after a Slash Command is executed, or is there a way to implement this? Error code:
/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640
throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^

DiscordAPIError[10062]: Unknown interaction
at handleErrors (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async BurstHandler.runRequest (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:736:23)
at async REST.request (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:1387:22)
at async ButtonInteraction.deferUpdate (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:200:5) {
requestBody: { files: undefined, json: { type: 6 } },
rawError: { message: 'Unknown interaction', code: 10062 },
code: 10062,
status: 404,
method: 'POST',
url: 'https://discord.com/api/v10/interactions/---------/-------------------/callback'
}
/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640
throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^

DiscordAPIError[10062]: Unknown interaction
at handleErrors (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async BurstHandler.runRequest (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:736:23)
at async REST.request (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:1387:22)
at async ButtonInteraction.deferUpdate (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:200:5) {
requestBody: { files: undefined, json: { type: 6 } },
rawError: { message: 'Unknown interaction', code: 10062 },
code: 10062,
status: 404,
method: 'POST',
url: 'https://discord.com/api/v10/interactions/---------/-------------------/callback'
}
Code:
/*
createCollector is used when receiving a button interaction.
createModal is a response to the new interaction.
*/
async createCollector (currentSetup, msgInt, channel, member, client) {
// Create a collector
if (!currentSetup.components) return;

const collector = msgInt.channel?.createMessageComponentCollector({
max: 1,
time: 1000 * 60,
})

// Add a listener to the collector
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})

async createModal(modal, interaction) {
// Generate modal
const modalBuilder = new ModalBuilder()
.setCustomId(modal.id)
.setTitle(modal.title);

// Generate inputs
for (let input of modal.inputs) {
let modalElement = new TextInputBuilder()
.setCustomId(input.id)
.setLabel(input.label);

modal.longForm ?
modalElement.setStyle(TextInputStyle.Paragraph) :
modalElement.setStyle(TextInputStyle.Short);

if (input.placeholder) modalElement.setPlaceholder(input.placeholder);
if (input.required) modalElement.setRequired(true);

const actionRow = new ActionRowBuilder().addComponents(modalElement);
modalBuilder.addComponents(actionRow);
}

// Send modal
await interaction.showModal(modalBuilder);
}
/*
createCollector is used when receiving a button interaction.
createModal is a response to the new interaction.
*/
async createCollector (currentSetup, msgInt, channel, member, client) {
// Create a collector
if (!currentSetup.components) return;

const collector = msgInt.channel?.createMessageComponentCollector({
max: 1,
time: 1000 * 60,
})

// Add a listener to the collector
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})

async createModal(modal, interaction) {
// Generate modal
const modalBuilder = new ModalBuilder()
.setCustomId(modal.id)
.setTitle(modal.title);

// Generate inputs
for (let input of modal.inputs) {
let modalElement = new TextInputBuilder()
.setCustomId(input.id)
.setLabel(input.label);

modal.longForm ?
modalElement.setStyle(TextInputStyle.Paragraph) :
modalElement.setStyle(TextInputStyle.Short);

if (input.placeholder) modalElement.setPlaceholder(input.placeholder);
if (input.required) modalElement.setRequired(true);

const actionRow = new ActionRowBuilder().addComponents(modalElement);
modalBuilder.addComponents(actionRow);
}

// Send modal
await interaction.showModal(modalBuilder);
}
8 Replies
d.js toolkit
d.js toolkit12mo ago
- What's your exact discord.js npm list discord.js and node node -v version? - Not a discord.js issue? Check out #other-js-ts. - Consider reading #how-to-get-help to improve your question! - Explain what exactly your issue is. - Post the full error stack trace, not just the top part! - Show your code! - Issue solved? Press the button!
Honie
Honie12mo ago
Intended behaviour:
edocsil
edocsil12mo ago
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})
I'm not clear why you're calling i.deferUpdate() when the rest of the code before interaction.showModal() shouldn't take long to execute. You should be able to just remove that line without any issue. Calling collector.stop() also seems redundant since it has a max of 1 and would end anyways What you saw about not being able to defer modal submits is correct, however I don't think it actually has an effect on what you are trying to do in this case
Honie
Honie12mo ago
You're right about collector.stop() and the reply being edited not being necessary, but not deferring the update causes a This interaction failed message to appear after three seconds. The collector is awaiting a response, maybe?
Honie
Honie12mo ago
Honie
Honie12mo ago
(That being said, the modal error doesn't appear anymore when not deferring the update, so that's progress.)
vxsl
vxsl5mo ago
@Honie did you ever get this to work?
Honie
Honie5mo ago
I believe I did make it work, but ended up scrapping the feature - returning to the same modal more than once works inconsistently I'll dig into the code later and see if I can give you a sample