A slash option can offer a fixed list with .addChoices() — but that caps at 25 and can't change. When the list is long (every item in your shop, every open ticket) or built at runtime, you want autocomplete: suggestions that appear and filter as the user types. This recipe wires up an autocomplete option and answers it.
| At a glance | |
|---|---|
| You need | a working bot from the discord.js guide, and comfort with Slash commands in depth |
| Plan | free or premium — free runs while your session timer lasts |
| Time | about fifteen minutes |
Autocomplete adds a second round trip: as the user types, Discord asks your bot for suggestions; when they pick or finish, the command runs normally. So one command means handling two interaction kinds.
Mark the option
Add .setAutocomplete(true) to a string, integer, or number option. That's the only change to the builder — no .addChoices(), because you'll supply them live:
const {
Client, Events, GatewayIntentBits, SlashCommandBuilder,
} = require('discord.js');
const FRUITS = ['Apple', 'Apricot', 'Banana', 'Blueberry', 'Cherry',
'Grape', 'Mango', 'Orange', 'Peach', 'Pear'];
const fruit = new SlashCommandBuilder()
.setName('fruit')
.setDescription('Pick a fruit')
.addStringOption(option =>
option.setName('name')
.setDescription('Start typing a fruit')
.setAutocomplete(true)
.setRequired(true));
Answer the suggestions
As the user types, an autocomplete interaction arrives. Guard it with isAutocomplete(), read what they've typed so far with getFocused(), filter your list, and send back up to 25 suggestions with interaction.respond():
client.on(Events.InteractionCreate, async (interaction) => {
if (interaction.isAutocomplete()) {
const focused = interaction.options.getFocused().toLowerCase();
const matches = FRUITS
.filter(f => f.toLowerCase().startsWith(focused))
.slice(0, 25);
await interaction.respond(matches.map(f => ({ name: f, value: f })));
return;
}
if (interaction.isChatInputCommand() && interaction.commandName === 'fruit') {
await interaction.reply(`You picked ${interaction.options.getString('name')}.`);
}
});
The moving parts:
getFocused()returns the text typed so far in the option currently being filled — an empty string when they've typed nothing yet. (UsegetFocused(true)to get{ name, value }when a command has more than one autocomplete option and you need to know which one.)respond([...])sends the suggestions. Each is a{ name, value }pair: name is what the user sees in the dropdown, value is what your command receives when they pick it — they can differ (show"Apple 🍎", send"apple")..slice(0, 25)matters: Discord accepts at most 25 suggestions and rejects the response if you send more.
Then the normal command handler reads the chosen value with getString('name') exactly as any option.
⚠️ Heads up: An autocomplete response has the same ~3-second deadline as any interaction, and you cannot
deferReply()it. Keep the filter fast — search an in-memory array or a quick indexed query, not a slow API call on every keystroke.
Suggestions from real data
The point of autocomplete is a list you couldn't hardcode. Swap the static array for anything: rows from your SQLite database, open tickets, a user's own items. The shape stays the same — filter by focused, cap at 25, map to { name, value }:
// Suggesting the caller's own to-do items from a database
const focused = interaction.options.getFocused().toLowerCase();
const items = getUserItems(interaction.user.id) // your query
.filter(row => row.title.toLowerCase().includes(focused))
.slice(0, 25);
await interaction.respond(items.map(row => ({ name: row.title, value: String(row.id) })));
Note the value is the row's ID while the name is the human title — so your command handler gets a clean ID to look up, not the display text.
Autocomplete, choices, or a select menu?
| Use... | when... |
|---|---|
.addChoices() |
a short fixed list (≤25) that never changes — difficulty, region |
| Autocomplete | a long or runtime list the user filters by typing |
| Select menu | the user picks after the command runs, from options you present in the reply |
Verify it works
Register /fruit, start the bot, and type /fruit name: followed by a letter. Suggestions filter as you type — ap narrows to Apple and Apricot. Pick one and the command replies with it. If the dropdown stays empty, your respond() isn't running or is sending nothing that matches.
Troubleshooting
- No suggestions appear — the
isAutocomplete()branch is missing, throws, or returns more than 25 items. Check the console for an error and confirm the.slice(0, 25). Interaction has already been acknowledged— you calledrespond()twice, or trieddeferReply(). Respond exactly once per autocomplete interaction, and never defer it.- The option won't autocomplete at all —
.setAutocomplete(true)and.addChoices()are mutually exclusive; a builder can't have both on the same option. - Wrong value reaches the command — you set
valueto the display text instead of an ID. The command readsvalue, so put the machine-friendly string there.