Handling Slow Slash Commands in Discord.js with deferReply and editReply
Learn how to avoid Discord's "Unknown Interaction" error by using deferReply and editReply to handle slow slash commands in Discord.js.
26 Apr 2026, 12:15 UTC

The problem: a slash command that takes too long
When a user invokes a slash command, Discord expects an initial response within 3 seconds. If your handler does heavy work (e.g., querying a database, calling an external API) before replying, the interaction token expires and the client shows an "Unknown Interaction" error. The user sees the "thinking…" spinner and then gets no reply.
Thesis: treat the interaction token as a short‑lived lease
The token granted by Discord is only valid for a brief window. Your first job in the handler is to acknowledge receipt, then perform any asynchronous work, and finally update the original reply. This pattern guarantees the token stays usable while you finish the work.
Using deferReply, editReply and followUp
Discord.js provides three methods that work together:
interaction.deferReply(options)– acknowledges the interaction and returns a handle that lets you later edit the same message. The interaction token remains valid for up to 15 minutes after deferring.interaction.editReply(content)– updates the deferred message with the result of your work.interaction.followUp(content)– sends additional messages while the token is still usable; useful for multi‑step responses.
If you need the reply to be visible only to the invoking user, pass { ephemeral: true } to deferReply (or to followUp).
Worked example: a command that fetches user stats
// file: src/commands/stats.js
const { SlashCommandBuilder } = require('@discordjs/builders');
module.exports = {
data: new SlashCommandBuilder()
.setName('stats')
.setDescription('Shows statistics for a user')
.addUserOption(opt => opt.setName('target').setDescription('User to query').setRequired(true)),
async execute(interaction) {
// 1️⃣ Acknowledge immediately – this buys us time
await interaction.deferReply({ ephemeral: false });
try {
// 2️⃣ Do the async work (replace with your actual logic)
const target = interaction.options.getUser('target');
const stats = await fetchUserStats(target.id); // <-- async function
// 3️⃣ Edit the original reply with the result
await interaction.editReply({
content: `Stats for ${target.tag}:\n${JSON.stringify(stats, null, 2)}`
});
} catch (err) {
// 4️⃣ On error, edit with a concise failure message
await interaction.editReply({
content: '❌ Unable to retrieve stats. Please try again later.'
});
// Log the error for diagnostics (do not expose internals to the user)
console.error(`[stats] interactionId=${interaction.id} error=${err}`);
}
}
};
// Dummy async function – replace with real implementation
async function fetchUserStats(userId) {
// Simulate network delay
await new Promise(res => setTimeout(res, 2500));
return { messages: 1245, reactions: 98 };
}
Key points in the example:
- The handler calls
deferReplybefore any await that could take longer than 3 seconds. - All asynchronous work happens inside a
try/catchblock. - Exactly one call to
editReply(or a follow‑up) resolves the interaction. - The interaction ID is logged for troubleshooting without leaking details to the user.
Trade‑off and limitation
Deferring hides latency but does not eliminate cost:
- Long‑running jobs still consume bot resources and can hit Discord’s global rate limits if many users trigger them simultaneously.
- Ephemeral replies (
ephemeral: true) are visible only to the command invoker, which reduces auditability for moderators or logs. - If you need to show the result to everyone, avoid ephemeral mode and consider posting the result in a dedicated channel or using a follow‑up message.
Actionable checklist for your slash‑command handlers
- Defer first – call
interaction.deferReply()as the very first line ofexecute. - Wrap work in try/catch – ensures you always edit the reply, even on failure.
- Edit exactly once – use
editReplyfor the final outcome; usefollowUponly for supplemental messages. - Log interaction metadata – record
interaction.id,interaction.type, andinteraction.createdTimestampalongside any errors. - Verify locally – add an artificial delay (
await new Promise(r => setTimeout(r, 4000))) before replying, confirm the "Unknown Interaction" appears, then add the defer/edit pattern and confirm success.
By treating the interaction token as a short‑lived lease and following the defer‑edit pattern, you eliminate the dreaded "unknown interaction" failures while keeping the user experience responsive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.