Discord autocomplete is the feature that lets you serve instant, dynamic suggestions for a slash command option as the user types. A static choice list (choices) is capped at 25 entries and is completely fixed, whereas autocomplete lets you generate the suggestion list yourself for every keystroke: a live database search, results pulled from an API, or data filtered specifically for the user's server. In this article we'll build an autocomplete system from scratch with discord.js v14, covering every step from enabling autocomplete on a command option, to answering the interaction within the strict 3-second window, to filtering large datasets intelligently.
When do you need autocomplete?
Discord's fixed addChoices() method is perfect for small, unchanging lists: putting red, green, and blue on a "color" option, for example. But it falls short in these cases:
- The number of options exceeds 25 (Discord's hard cap for static choices).
- The list is dynamic: the user's own items, records in a database, live API results.
- The list is server- or user-specific: each server has different tags, each user their own playlist.
This is where autocomplete comes in. When the user starts typing, the bot receives the current input, runs its own logic, and returns up to 25 suggestions. Suggestions are purely visual; the user can still type and submit a value that isn't in the list, so you must always validate the incoming value when handling the command.
Enabling autocomplete on a command option
The first step is to add .setAutocomplete(true) to the relevant option in your slash command definition. An important constraint: you cannot use both setChoices and autocomplete on the same option; you have to pick one. Autocomplete only works on string, integer, and number options.
const { SlashCommandBuilder } = require('discord.js');
const data = new SlashCommandBuilder()
.setName('search')
.setDescription('Search for an item by name')
.addStringOption(option =>
option
.setName('name')
.setDescription('The name of the item to search for')
.setRequired(true)
.setAutocomplete(true)); // dynamic suggestions instead of static choices
module.exports = { data, execute };
Once you register the command with Discord (globally or per guild), this option switches into autocomplete mode. The moment the user starts typing into the option, Discord sends your bot an interaction of a different type than a normal command run, and we need to catch it in a separate place.
Answering the autocomplete interaction
The event that fires as the user types is not a ChatInputCommandInteraction but an AutocompleteInteraction. So in your interactionCreate listener you first need to branch on the type. You read the current value of the focused option with interaction.options.getFocused() and return suggestions with interaction.respond():
client.on('interactionCreate', async (interaction) => {
// Normal commands
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
return command?.execute(interaction);
}
// Autocomplete requests
if (interaction.isAutocomplete()) {
const command = client.commands.get(interaction.commandName);
if (command?.autocomplete) {
try {
await command.autocomplete(interaction);
} catch (err) {
console.error('Autocomplete error:', err);
}
}
}
});
We add an autocomplete function to each command file, alongside the usual execute function. That way each command keeps its own suggestion logic self-contained:
async function autocomplete(interaction) {
const focused = interaction.options.getFocused(); // the text the user typed (string)
const items = ['Sword', 'Shield', 'Potion', 'Bow', 'Staff', 'Armor'];
const filtered = items
.filter(item => item.toLowerCase().startsWith(focused.toLowerCase()))
.slice(0, 25); // Discord accepts at most 25 suggestions
await interaction.respond(
filtered.map(item => ({ name: item, value: item }))
);
}
Each suggestion is an object of the form { name, value }. name is the label the user sees (max 100 characters), while value is the actual value you'll receive in execute when the command runs. The value type must match the option type: a string for a string option, a number for an integer option. This separation is very handy; you can show "Username" to the user while sending a real id as the value behind the scenes.
Generating dynamic suggestions from a database
Autocomplete's real power is feeding suggestions from a live data source. Say we suggest tags the user created themselves. Doing the search on the SQL side with LIKE and capping the result at 25 directly stays fast even across thousands of records:
async function autocomplete(interaction) {
const focused = interaction.options.getFocused();
// better-sqlite3 example — only this server's tags
const rows = db.prepare(
`SELECT name, id FROM tags
WHERE guild_id = ? AND name LIKE ?
ORDER BY name LIMIT 25`
).all(interaction.guildId, `${focused}%`);
await interaction.respond(
rows.map(row => ({ name: row.name, value: String(row.id) }))
);
}
There are two critical points here. First, let the database do the filtering: pulling all records into memory and sifting them in JavaScript is slow on large tables; LIKE ? and LIMIT 25 hand the work to the DB. Second, show a readable name (name) to the user while sending the unique id (id) as the value; when you handle the command you find the record directly, without having to search again.
The 3-second rule, limits, and performance
There are strict rules you must respect with autocomplete:
- The 3-second limit: you must answer the interaction with
respond()within three seconds, or Discord drops the request.deferReplydoes not work here; it can't be deferred. So your suggestion query has to be fast. - The 25-item cap: always trim the array with
.slice(0, 25)or SQLLIMIT 25; anything more throws an error. - Empty input: if the user hasn't typed anything yet,
focusedis an empty string. In that case, showing the most popular or most recent items gives a good default experience.
While the user types quickly, Discord may send a new request on every keystroke. To keep these requests from straining your bot, two techniques help: a short-lived cache for frequently accessed results (for example a Map that holds a query result for a few seconds), and rate-limiting your requests if you use an external API. Still, remember: value validation belongs in execute, because the user isn't forced to pick from the list; they can type and submit by hand.
Frequently Asked Questions
Can I use autocomplete and setChoices together?
No. On a single option you use either static setChoices() or setAutocomplete(true); the two can't coexist, and the command registration is rejected if you try. If your list has fewer than 25 entries and never changes, setChoices is simpler. If the list is large, dynamic, or personalized, switch to autocomplete.
Can I be sure the user picked one of the suggestions?
No, you can't guarantee it. Suggestions are merely a convenience; the user can type any text that isn't in the suggestions and press Enter. So always validate the incoming value in the command's actual execute function: check whether the record really exists and return a friendly error message if it doesn't.
How do I find out which focused option's name is being typed?
If a command has more than one autocomplete option, you need to know which one is focused. Calling interaction.options.getFocused(true) (passing true) returns not just the value but the { name, value } object; you read which option is being typed from the name field and generate suggestions accordingly.
Want smart, fast autocomplete commands for your bot? I can build commands with database-backed live search, caching, and server-specific suggestions for you. Get in touch and let's talk through what you need.