Troubleshooting
The failures most bots meet, what the bot or Discord says for each, what causes it, and where the fix is.
The failures most bots meet, what causes each, and where the fix is explained. Text in quotes is what Discord's app or the bot's log shows.
The bot won't start
"Discord token is missing: meocord.config.ts sets discordToken, and a new app reads it from DISCORD_TOKEN in
.env." The config gives no token. Check that .env exists where the bot runs, from its working directory, and
names DISCORD_TOKEN. See Environment variables.
"Discord refused the bot token." Discord refused the token at login, so start() rejects and the process exits
with code 1. Copy the token again from the Developer Portal, under your application, Bot, then Reset Token.
Resetting it there makes the old one stop working.
"Discord refused the privileged intents the bot requests (…)" clientOptions.intents asks for
GuildMembers, GuildPresences or MessageContent, and the application hasn't enabled them. Enable each one the
message names in the Developer Portal, under Bot, then Privileged Gateway Intents. A verified bot in 100 or more
servers needs Discord's approval for them. See Intents.
"Discord refused the intents the bot requests as invalid." A value in clientOptions.intents isn't one of
discord.js's GatewayIntentBits.
meocord.config.ts doesn't load. build, start and register stop with the file and line, or with a list
of every option of the wrong type. An option MeoCord doesn't know, often a typo, is only a warning. See
Configuration.
"MeoCord config not found at … (working directory …)." The bot looks for meocord.config.mjs beside its bundle,
and there's none. Run meocord build, and start the bot from the dist it writes.
"MeoCord config at … failed to load: …" The compiled config is there, but loading it threw, for the reason the message gives. When that's a package that isn't installed, the message names it:
MeoCord config at /srv/bot/dist/meocord.config.mjs failed to load: Cannot find package 'dotenv' imported from /srv/bot/dist/meocord.config.mjs. Install dotenv in the project, then run `meocord build`.When the config doesn't import the missing package itself, the message names the installed package that does, as
"Install name, which other imports, in the project". Otherwise it says to fix meocord.config.ts, then build
again.
One line naming a class, and exit code 1
MeoCord refuses a mistake it can see as the bot loads, and reports it without a stack. Its first line starts with what
it is on: Class.method:, Class:, the app class's name, such as App:, for @MeoCord's options,
meocord.config.ts: for its settings, or the build's folder, such as dist:. The rest names the decorator and the
problem, and a last line, in src/…, names the file it comes from when MeoCord can tell. Such as:
SampleButtonController.handleButtonWithId: Invalid pattern "button-with-{ownerId}": {ownerId} must occupy a whole segment, …Fix what it names. The common ones:
"
Class: parameter 1 of its constructor has no runtime type, so it cannot be created." A controller or service asks for a parameter MeoCord can't inject. Usually two classes import each other, and the message names the class that injects it. A parameter typed with an interface, or with animport type, reads the same way. Move what both need into a third service, or inject the parameter with@Inject(token).meocord/eslintwarns about such cycles as you write them; see Import cycles."
Class: it injects …, which nothing provides" A class injects a string, symbol orcreateTokentoken that no provider supplies. Add a provider for it to@MeoCord({ providers }), or to the testing module'sprovidersin a test. See Providers."App: @MeoCord({ providers }): the provider for '…' injects ExecutionContext, but its factory runs once and its value is shared, …" A factory provider lists
ExecutionContextin itsinject. Its value is made once for the whole app, so it would keep the first call's context for every later call. InjectExecutionContextinto a guard instead. The testing module refuses it the same way. A service, interceptor or filter that injects it reads "Class: resolved once and shared, so it cannot inject ExecutionContext", and auseClassprovider's class "the provider for '…' usesClass, which injects ExecutionContext"; the fix is the same."
Class: its constructor takes parameters, butClasshas no decorator, …" A class is injected, or injects, without its decorator, so TypeScript recorded none of its parameter types. Add what the line says:@Service(), or@Controller(),@Guard(),@Interceptor(),@Catch()or@Pipe()for the class's role. A class from a package gets a provider in@MeoCord({ providers })instead.Loggerand errors such asUserErrorread "MeoCord does not inject it"; create them withnew."
Class: @Command goes on a method, not on a class." A decorator is on the wrong target, written directly or throughapplyDecorators. Handler decorators such as@Command,@MessageHandler,@On,@Deferand@Validatego on a method;@Observer,@Interceptor,@Catchand@Pipego on a class, and on a method read "Class.method: @Observer goes on a class, not on a method." Move it."'a' → 'b' → 'a': each is made before what injects it, so none of them can be made." Providers or classes inject each other in a cycle, which the line names from where it was entered. Move what they share into a provider of its own. See Providers.
"
Class: two classes have this name; …" MeoCord tells these classes apart by name, and the rest of the line says why:text Shop: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share their counts. Rename one of them.Two classes of one name are refused when either uses
@Cooldownor@Once, in any mode. With process sharding, any two controllers, services or provided classes are, sinceShardContext.callfinds a class in another shard by its name. Rename one of the classes."
Class.method: @MessageHandler('…'): …" A message pattern MeoCord can't read stops the bot there, such as a rest that isn't last, a type nothing adds, a name used twice, or braces inside a word. So doesscope: 'dm'on a command with amember,roleorchannelparam. Two patterns that match the same messages stop it too, with a line naming both handlers and their patterns, such asA.swap: "swap {a}" and "swap {b}" in B.swap match the same messages, …. See Errors at startup, which lists each one."
Class.method: "…" and "…" inOther.methodmatch the same … customIds" Two component handlers of one type take the same ids, so only one could ever run.MeoCordFactory.create(),meocord registerandMeoCordTestingModule.compile()refuse them,registerbefore it sends any command. Change one pattern; see Overlapping patterns."
Class.method: @Validate and @UsePipe are for interaction and patterned message handlers, …" They check a handler's options, customId params, modal fields or pattern params, and a message handler without a pattern, a reaction, autocomplete or event handler has none.@Cooldownon a reaction, autocomplete or event handler is refused the same way, "@Cooldownis for interaction and message handlers"; on the controller, it skips them."
Class: not decorated with @MeoCord(), so there is no app to create." The class given toMeoCordFactory.create(), usually insrc/main.ts, has no@MeoCord."meocord.config.ts: sharding.mode 'process' starts one shard per process, …" With process sharding,
clientOptions.shardsandshardCountmust be unset. Otherwise, set there, they must agree withsharding.shards, or the line reads "sharding.shards (…) and clientOptions.shards/shardCount disagree"; set the shards in one place. See Sharding."dist: this build carries native addons compiled for …, but is running on …" A self-contained build made on one platform was started on another. Build where it runs; for a container, run
meocord buildinside the image.A builder that fails. A command's builder runs as its class loads, so a name Discord refuses, such as one with a capital letter or a space, stops the bot there, naming the builder and the command. See Your first command. A builder whose constructor throws, such as one that reads a translator in a field, stops it the same way, with the builder's error last:
text Stats.stats: StatsBuilder could not be made for "stats": missing translator.
Other startup errors
What MeoCord can't see as the bot loads reaches the generated main.ts, which logs it as "Error during startup:" with
the error, and the process exits 1. A failure MeoCord explains itself, such as a provider that fails, is logged once, in
MeoCord's words, and main.ts doesn't log it again.
"The factory providing … failed: …" A factory in @MeoCord({ providers }) threw or rejected, such as a database
refusing the connection, so the bot stopped before login with the cause. Fix what the cause names; see
Providers.
Warnings that become errors in 5.0
Some mistakes a 4.0 bot could start with only warn in 4.1, so the bot still starts, and stop it in the next major version (5.0). Each warning names the handler or the filter, and says what to write instead:
- "Broken: @Catch's first entry, undefined, which matches no error, is deprecated; …" An entry in a filter's
@Catchisn't a class, often anundefinedfrom two files that import each other. The filter still catches the other types it lists. Import the class where it's defined, or move it out of the cycle. See Exception filters. - "@MessageHandler('') on
Class.methodis deprecated; in the next major version (5.0) it is refused." An empty pattern runs for every message, as no pattern does. Write@MessageHandler()for a listener, or check the value the pattern is built from. - "… never run: MeoCord dispatches only to @MeoCord({ controllers })." A command, component, message or reaction
handler sits on a service or another class outside
@MeoCord({ controllers }). Move it to a controller; see Messages and reactions. - "@Service on the method
Class.methodis deprecated; in the next major version (5.0) it is refused."@Controller,@Service,@Guard,@CommandBuilderor@MeoCordis on a method, where it does nothing. Move it to the class.
The upgrade guide lists each of these warnings, with what to change. A warning that ends "will be removed in the next major version (5.0). Use … instead." names an API that still works, and is logged once. See the upgrade guide.
A command doesn't show up in Discord
- It was registered somewhere else. Under
meocord start --devwithcommands.developmentGuildset, every command goes to that server only. Otherwise commands go globally, or tocommands.guilds. See Registering commands. - Registration didn't run. With
commands.register: false, onlymeocord registerregisters. A builder whosetoJSON()throws, such as a slash command without a description, stops that start's registration with an error naming it, and no command is sent; see the upgrade note. A failed registration is logged, and the bot stays online. - The bot isn't in the server with the right scope. An invite must include the
applications.commandsscope as well asbot. - The client hasn't caught up. Server commands appear at once; global ones can take a while to reach every client. Reloading Discord, with Ctrl+R or Cmd+R, refreshes the command list.
A command you removed that still shows is a leftover in a scope this configuration doesn't register to. The bot
warns "… command(s) are still registered … which this configuration does not register to", and
commands.clearOther removes them.
"The application did not respond"
Discord gives an interaction three seconds for its first answer, and nothing arrived in time:
- The handler is slow. A database call or an API request before the first reply can take longer than three
seconds. Add
@Defer(), which acknowledges first. - A guard returned
false. That stops the call without an answer, on purpose. Under@Defer, a command's deferred reply is deleted. To tell the user why, throwGuardDeniedErrorinstead. See Guards. - An exception filter caught the error and sent nothing. MeoCord's built-in fallback answers an error no exception filter handles, but a filter that handles one and sends nothing leaves the interaction unanswered.
- The handler never answered. In development, MeoCord warns once for each handler that ends without answering, or defers and never follows up, and names it. An interceptor that returned before the handler ran or finished, the outermost when several did, is named instead: "Shop.buy: its interceptor Cached returned before the handler ran, without answering the interaction, …". See Responses.
A test shows the guard case: the call doesn't run, and nothing is sent.
it('runs in an allowed channel, and a guard that returns false stops it silently', async () => {
const allowed = createMockInteraction(ChatInputCommandInteraction, { channelId: '111111111111111111' })
const elsewhere = createMockInteraction(ChatInputCommandInteraction, { channelId: '222222222222222222' })
await expect(module.invoke(ModerationSlashController, 'trade', allowed)).resolves.toEqual({ ran: true })
await expect(module.invoke(ModerationSlashController, 'trade', elsewhere)).resolves.toEqual({ ran: false })
expect(getResponse(elsewhere).sent).toBe(false)
})A button, select menu or modal that no route takes is answered "Command not found!", and the log names its
customId. See When nothing matches.
An autocomplete interaction can't be deferred: it has three seconds to answer, once. Keep its handler to a cache lookup. See Autocomplete.
Discord API errors
| Code | Discord's message | What happened |
|---|---|---|
| 10062 | Unknown interaction | The first answer came after the three seconds. Use @Defer(). |
| 40060 | Interaction has already been acknowledged | Two answers were sent as first answers, often a reply after a deferReply. respond() picks the right call. |
| 50027 | Invalid Webhook Token | A follow-up or an edit came more than fifteen minutes after the interaction, when its token expires. Send a new message instead. |
| 50001 | Missing Access | The bot can't see the channel. Check its role and the channel's permissions. |
| 50013 | Missing Permissions | The bot can see the channel but lacks the permission the call needs, such as Manage Messages to delete one. |
When one of MeoCord's own answers is refused, such as an error reply or a usage reply, the refusal is logged at debug
level when it's a state Discord reports, like these. A body Discord could not read is logged as an error, with its
cause, since only the code that built it can fix it: 50035 (Invalid Form Body), 50109 (invalid JSON) or 50006
(an empty message).
A test can make a mock reject with any of them through createDiscordError(code);
see Discord's errors.
Messages and reactions
- A message command never runs. In a server, the bot needs the
GuildMessagesintent, andMessageContent, which is privileged, to read a message's text; a mention-only command needs noMessageContent, and ascope: 'dm'command needsDirectMessagesandPartials.Channelinstead. The bot warns at startup about what's missing. Messages from bots never reach a handler. A pattern matches after the app's prefix, or the handler's own, and only the most specific matching pattern runs. See Which handler runs. - Reactions are missed. Reactions need the
GuildMessageReactionsintent, orDirectMessageReactionsin DMs. For reactions on messages sent before the bot started, add theMessageandReactionpartials. See Reactions. - A handler on a service never runs. Commands, components, messages and reactions reach only the app's
@MeoCord({ controllers }). The bot warns at startup, "… never run: MeoCord dispatches only to @MeoCord({ controllers })", or "never runs" for one handler, naming each one; move it to a controller. See the upgrade guide. @On(event)never runs. Most events need an intent. The bot warns at startup, "The … intent is not in clientOptions.intents, so Discord will not send what … handles", naming the handler. See Intents.
Sharding
- One-off work runs once per shard. Guard it with
onReady'sprimary, which istrueonly in the process running shard 0. See A process per shard. - A shard keeps restarting. The manager restarts a shard that exits, waiting longer each time up to a minute. One that can't log in because of the token or its intents stops the bot instead. Read the shard's first error in the log.