aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Bot Error Handling: try/except and Global Handlers

A Discord bot error usually shows up at the worst possible moment: late at night, when users are most active, the bot suddenly stops responding to commands, a long traceback fills the terminal, and the process dies. The good news is that the vast majority of these situations are predictable, and with proper error handling you can keep your bot alive. In this article I'll cover the common error types, command-level try/except usage, setting up a global error handler, and proper logging — on both the discord.py and discord.js sides.

Why error handling matters

A bot is a long-running process. A single uncaught exception in one command will, depending on the framework, either fail that command silently or, in the worst case, take down the whole bot. To the user, that just reads as "the bot is broken." A solid error-handling strategy does three things:

  • Isolates: an error in one command doesn't affect the others.
  • Informs: the user gets a meaningful message, not a traceback.
  • Records: as the developer, you can see in the logs what broke and why.

The most common errors

Before you can handle errors, you need to recognize them. In practice these are the ones you'll meet most:

  • Permission errors: the bot tries to post in a channel or assign a role without the rights to do so. In discord.py this is discord.Forbidden; in discord.js it's 50013 Missing Permissions.
  • Rate limit (429): too many requests in too short a window. The libraries usually queue these automatically, but watch out when sending messages inside a loop.
  • Resource not found (404): reacting to a deleted message, fetching a member that no longer exists.
  • Invalid user input: text where a number was expected, a missing argument. In discord.py these are BadArgument / MissingRequiredArgument.
  • Token / gateway issues: a wrong token, or privileged intents that were never enabled.

Command-level try/except

The first line of defense is a try/except block wrapping the risky operation directly. The key rule: never use a bare except: — always catch the specific exception you expect, and let unexpected ones bubble up so the global handler and logs can see them.

import discord
from discord.ext import commands

bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())

@bot.command()
async def kick(ctx, member: discord.Member):
    try:
        await member.kick(reason="Moderator decision")
        await ctx.send(f"{member} was removed from the server.")
    except discord.Forbidden:
        await ctx.send("I don't have permission to kick this member.")
    except discord.HTTPException as e:
        await ctx.send("A network error occurred during the operation.")
        raise  # re-raise so the logger and global handler see it

Here we explain the two known cases (no permission, network error) to the user; in the discord.HTTPException branch we re-raise after replying, so the error still reaches the logs.

Setting up a global error handler

Writing a try/except in every single command is not sustainable. Instead, define a central error catcher. In discord.py that's the on_command_error event:

import logging
logger = logging.getLogger("bot")

@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.MissingRequiredArgument):
        await ctx.send(f"Missing argument: {error.param.name}")
    elif isinstance(error, commands.BadArgument):
        await ctx.send("You entered an invalid value.")
    elif isinstance(error, commands.CommandOnCooldown):
        await ctx.send(f"Please wait {error.retry_after:.1f}s.")
    elif isinstance(error, commands.MissingPermissions):
        await ctx.send("You don't have permission for this command.")
    else:
        logger.exception("Unexpected error", exc_info=error)
        await ctx.send("An unexpected error occurred and was logged.")

If you use slash commands (app commands), you'll need to assign a separate handler via tree.on_error; the classic on_command_error for prefix commands does not cover slash commands.

On the discord.js side the logic is similar: you listen to the client event and also handle unhandled rejected promises:

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand()) return;
  try {
    await handleCommand(interaction);
  } catch (err) {
    console.error(err);
    const reply = { content: 'An error occurred.', ephemeral: true };
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp(reply);
    } else {
      await interaction.reply(reply);
    }
  }
});

process.on('unhandledRejection', err => console.error('Unhandled:', err));

Proper logging

Working with print() looks tempting at first, but it's useless in production. Use Python's built-in logging module: it gives you levels (INFO, WARNING, ERROR), timestamps, and writing to a file.

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    handlers=[
        logging.FileHandler("bot.log", encoding="utf-8"),
        logging.StreamHandler(),
    ],
)

A few practical tips:

  • Use logger.exception(...) where you catch the error; it attaches the traceback automatically.
  • Never log secrets such as tokens or passwords.
  • Rotate your log files (RotatingFileHandler) so the disk doesn't fill up.
  • Sending critical errors to a dedicated Discord webhook channel is a practical way to catch problems early.

Staying up without crashing

Even with good handlers, your bot may go down one day. So run the process under a supervisor: a systemd service on Linux, or a process manager like pm2, will restart the bot automatically when it stops. The goal is not to hide the crash but to read the logs, fix the root cause, and treat the supervisor only as a safety net.

Frequently Asked Questions

Should I wrap every command in try/except?

No. Use a local try/except only where you want a custom user message, or around a known risky operation (deleting messages, assigning roles). For everything else, the central on_command_error is enough.

Why aren't errors in slash commands being caught?

Because on_command_error only covers prefix commands. For slash (app) commands you must separately define the command tree's own error handler (tree.on_error).

I keep getting rate limit errors — what should I do?

Avoid sending messages back-to-back inside a loop, split bulk operations into batches, and trust the library's built-in queue. Rather than slowing every request with a manual sleep, group your operations.

Is your bot constantly crashing, or can't you track its errors? I can help you set up solid error handling and logging and get your bot production-ready. Get in touch with me.

Bu kategorideki tüm yazılar →

Devamı için