Description
Problem
Currently, all of the error handling is usually done through an ErrorHandler
Cog.
This cog will only handle errors raised from Text Commands
and will not catch errors raised from App Commands
(Slash Commands
and Context Menu Commands
)
Solution
The way we can centralize handling for App Commands
would ideally be by subclassing the CommandTree
class and override the on_error
method, then upon instantiating the bot, will pass this class as an argument to the tree_cls
param.
We obviously want to factor out the commun error handling code to avoid duplication, but not at the cost of code simplicity/readability. What this means is that a little bit of duplication is ok instead of having alot of python magic thrown at it.
I wanted to discuss the approach I had in mind before I start working on this.
Idea
- Basically, this would be just like middlewares in most web frameworks, implemented as a
chain of error handlers
- Each concrete error handler would implement a specific interface that would allow it to know
- a. Whether it should handle the error in the first place
- b. How to handle the error
- These handlers would then be registered inside a container class that would loop over all the handlers.
Once the previous is done, the ErrorHandler
cog and the CommandTree
subclass would take an instance of this container, and let it do all the work.
This approach would allow a more flexible design of each error handler, and a clearer separation of responsibilities instead of all the if/else
s we currently have chained together.
Error handler interface
The interface of an error handler look something like this
# pydis_core.utils.error_handling.abc
from abc import ABC, abstractmethod
from discord import Interaction, errors
from discord.ext.commands import Context
class AbstractCommandErrorHandler(ABC):
"""An abstract command error handler"""
@abstractmethod
def should_handle_error(self, error: errors.DiscordException) -> bool:
"""A predicate that determines whether the error should be handled or not."""
@abstractmethod
async def handle_app_command_error(self, interaction: Interaction, error: errors.DiscordException):
"""Handle error raised in the context of app commands."""
@abstractmethod
async def handle_text_command_error(self, context: Context, error: errors.DiscordException):
"""Handle error raised in the context of text commands."""
Concrete error handler
This is just a reimplementation of the current check failure error handler we have in our bot
repo.
# pydis_core.utils.error_handling.handlers.check_failure
from pydis_core.utils.error_handling.abc import AbstractCommandErrorHandler
from discord.ext.commands import errors
class CheckFailureErrorHandler(AbstractCommandErrorHandler):
def __init__(self, bot):
self.bot = bot
def should_handle_error(self, error: errors.DiscordException) -> bool:
if isinstance(error, errors.CheckFailure):
return True
return False
async def handle_app_command_error(self, interaction: Interaction, error: errors.DiscordException):
await self._handle_error(error=error, context=None, interaction=interaction)
async def handle_text_command_error(self, context: Context, error: errors.DiscordException):
await self._handle_error(error=error, context=context, interaction=None)
async def _handle_error(
self,
error: errors.DiscordException,
context: Context = None,
interaction: Interaction = None
):
bot_missing_errors = (
errors.BotMissingPermissions,
errors.BotMissingRole,
errors.BotMissingAnyRole
)
if isinstance(error, bot_missing_errors):
self.bot.stats.incr("errors.bot_permission_error")
if context:
await context.send("Sorry, it looks like I don't have the permissions or roles I need to do that.")
if interaction:
await interaction.response.send_message(
"Sorry, it looks like I don't have the permissions or roles I need to do that."
)
elif isinstance(error, errors.ContextCheckFailure | errors.NoPrivateMessage):
self.bot.stats.incr("errors.wrong_channel_or_dm_error")
if context:
await context.send(str(error))
if interaction:
await interaction.response.send_message(str(error))
Error handlers' container
This class would register all error handlers, and allow to loop overthem to look for the handler that would fit.
We could register handlers under specific names if we ever want to have some "overriding" of error handlers in client apps.
# pydis_core.utils.error_handling.container
class ErrorHandlerContainer:
def __init__(self, handlers: list[AbstractCommandErrorHandler] = None):
self.handlers = handlers if handlers else []
async def handle_error(
self,
error: errors.DiscordException,
context_or_interaction: Context | Interaction
) -> None:
for handler in self.handlers:
if not handler.should_handle_error(error):
continue
if isinstance(context_or_interaction, Context):
await handler.handle_text_command_error(context_or_interaction, error)
if isinstance(context_or_interaction, Interaction):
await handler.handle_app_command_error(context_or_interaction, error)
Usage inside a cog
Once all the handlers are implemented, the error handler cog would be simplified to the following.
class ErrorHandler(Cog):
def __init__(self, bot, error_handlers_container):
self.bot = bot
self.error_handlers_container = error_handlers_container
@Cog.listener()
async def on_command_error(self, ctx: Context, e: errors.CommandError) -> None:
await self.error_handlers_container.handle_error(e, ctx)
Things that can be added on top of the previous
- It goes without saying that a default handler needs to be implemented.
- The order of handlers is important, so we can either rely on some sort of priority system, or just the order of registering them"