Discord.js modals failing with generic error after working fine

Modal submission issues with Discord bot

I’ve got a Discord bot built with discord.js that uses several modals triggered by buttons in embedded messages. Everything was working perfectly for about a month.

Now all the modals are broken. When users try to submit any modal, they get the generic “Something went wrong. Try again.” error message. I haven’t changed any code recently. I even tried running the bot on a completely different server but the same problem happens.

The weird part is that the bot responds to everything else normally. The error shows up really fast (under 2 seconds). I have console logs that fire when the modal opens and when someone hits Submit. The problem is my server only sees the first log - it’s like the submit action never reaches my code.

Here’s the main interaction handler:

export async function handleInteraction(
    botClient: CustomClient,
    userInteraction: Interaction,
    rewardContract: TokenDistribution,
    taskQueue: ProcessingQueue
): Promise<void> {
    const getTimestamp = (): string => new Date(Date.now()).toUTCString();
    const logChannel = botClient.channels.cache.get(channels.logs) as TextChannel;

    try {
        console.log("Interaction received");

        // Verify user isn't rate limited
        const allowedToProceed = await validateUserCooldown(userInteraction.user.id);
        if (!allowedToProceed) {
            if (userInteraction.isRepliable()) {
                await userInteraction.reply({
                    content: "Please wait before trying again. You're doing that too fast.",
                    ephemeral: true,
                });
            }
            return;
        }

        // Handle slash commands
        if (userInteraction.isChatInputCommand()) {
            await userInteraction.deferReply({ ephemeral: true });
            if (userInteraction.commandName === "rewards") {
                await showMainInterface(userInteraction, rewardContract);
            } else {
                await userInteraction.editReply({
                    content: "Unknown command detected!",
                });
                await logChannel.send(
                    `[WARNING]\n${getTimestamp()}\nUnknown Command\nUser: ${
                        userInteraction.user.username
                    }`
                );
            }
        } else if (userInteraction.isButton()) {
            // Handle button clicks
            if (userInteraction.customId === "search_wallet") {
                await processWalletSearch(userInteraction, rewardContract);
            } else if (userInteraction.customId === "configure_network") {
                await handleNetworkConfig(userInteraction, rewardContract, taskQueue);
            } else if (userInteraction.customId === "update_username") {
                await processUsernameUpdate(userInteraction, rewardContract, taskQueue);
            } else if (userInteraction.customId === "reset_results") {
                await userInteraction.deferUpdate();
                await clearUserResults(userInteraction, rewardContract);
            } else if (userInteraction.customId === "start_process") {
                await userInteraction.deferReply({ ephemeral: true });
                await initiateClaimProcess(userInteraction, rewardContract, taskQueue);
            } else {
                await userInteraction.reply({
                    content: "This button isn't working right now!",
                    ephemeral: true,
                });
                await logChannel.send(
                    `[WARNING]\n${getTimestamp()}\nUnknown Button\nUser: ${
                        userInteraction.user.username
                    }`
                );
            }
        } else if (userInteraction.isModalSubmit()) {
            // Handle modal submissions
            await userInteraction.deferReply({ ephemeral: true });

            if (userInteraction.customId === "network_config_form") {
                await handleNetworkConfig(userInteraction, rewardContract, taskQueue);
            } else if (userInteraction.customId === "address_input_form") {
                await processWalletSearch(userInteraction, rewardContract);
            } else if (userInteraction.customId === "username_update_form") {
                await processUsernameUpdate(userInteraction, rewardContract, taskQueue);
            } else {
                await userInteraction.editReply({
                    content: "This form isn't configured properly!",
                });
                await logChannel.send(
                    `[WARNING]\n${getTimestamp()}\nUnknown Modal\nUser: ${
                        userInteraction.user.username
                    }`
                );
            }
        } else {
            await logChannel.send(
                `[WARNING]\n${getTimestamp()}\nUnknown Interaction\nUser: ${
                    userInteraction.user.username
                }`
            );
        }
    } catch (err) {
        console.error(`Interaction Error: ${err}`);
        await logChannel.send(`[ERROR]\n${getTimestamp()}\nInteraction Failed\n${err}`);
        if (userInteraction.isRepliable()) {
            await userInteraction.editReply({
                content: `Error occurred: ${err.message}`,
            });
        }
    }
}

And here’s the function that handles the wallet search:

export async function processWalletSearch(
    userInteraction: ModalSubmitInteraction | ButtonInteraction,
    rewardContract: TokenDistribution
): Promise<void> {
    if (userInteraction.isButton()) {
        try {
            console.log(
                "Wallet search button clicked by " +
                    userInteraction.user.username +
                    " - " +
                    userInteraction.user.id
            );
            const inputForm = await buildWalletInputModal();
            await userInteraction.showModal(inputForm);
            await userInteraction.deleteReply();
        } catch (err) {
            console.log(err);
        }
    } else if (userInteraction.isModalSubmit()) {
        const currentUserId = userInteraction.user.id;
        console.log(
            "Wallet search form submitted by " + userInteraction.user.username + " - " + currentUserId
        );
        // ... rest of processing
    }
}

Anyone else run into this? I’m completely stuck on what could cause modals to just stop working like this.

This looks like a Discord API timeout, not your code. Your interaction handler logs the modal opening but not the submit - Discord isn’t routing the submission back to your bot at all. I’ve seen this when Discord’s gateway connection gets unstable or there’s too much latency between your bot and Discord’s servers. Add more detailed logging around your websocket connection status and check for any disconnect/reconnect events in your client logs. Also check if your bot token permissions changed recently - Discord sometimes revokes permissions silently, which breaks modal submissions while other interactions keep working. That 2-second timeout is suspiciously fast and usually means network-level rejection, not code failure.

same thing happened to me last week. check your discord.js version - they broke modal handling in recent updates. that fast error usually means your modal customId’s are getting mixed up. try adding timestamps to your modal IDs like address_input_form_${Date.now()} and update your handler to use startsWith() instead of exact matches. fixed my bot when nothing else worked.

I hit this exact same issue two months ago and it drove me nuts for days. The problem’s probably your await userInteraction.deleteReply() line in the button handler. When you call deleteReply() on a modal interaction, it messes with Discord’s ability to handle the modal submission properly. Try commenting out that await userInteraction.deleteReply() line in your processWalletSearch function and see if it fixes it. Deleting the reply before the modal workflow finishes screws up the interaction context. Also noticed you’re not deferring the button interaction before showing the modal - that causes timing issues. Add await userInteraction.deferUpdate() before showModal() instead of deleting the reply afterward.