Skip to content

Centralize error handling #198

Closed
@shtlrs

Description

@shtlrs

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/elses 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"

Metadata

Metadata

Assignees

Labels

a: codePull requests which add features, fixes, or any code changes: planningA feature being considered or discussedt: enhancement

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions