Skip to content
Bluenix edited this page Apr 5, 2022 · 1 revision

This is separate document - geared to developers interested in the library's design choices - documentating the error handling API.

Brief Abstract

It is very common to find a system of error handlers in programs such as web servers and Discord bots, this is because you usually want these to keep running no matter what.

The point of an error handler is to allow the user to handle an error as they wish - for example sending it to Sentry or invoking a webhook.

Other APIs

Let's start like was done for the dispatch and extension API by looking at other wrappers and compare pros and cons.

discord.py

Discord.py's error handler is part of the event and dispatch API. You define a function called on_error() and add it as a listener - it is then called with the method that raised an error and its arguments.

Important to note is that the error isn't given directly, which is a big downside if you wish to for example grab an attribute of the error. To get the error you need to call sys.exc_info().

The separate ext.commands system does have one big benefit by allowing per-command error handling.

Hikari

Hikari also relies the event system - you annotate the callback with ExceptionEvent and it will be called when any event handler experiences an error.

There is not a way to setup a per-callback error handler, instead you need to compare the failed_callback property to the callback you wish to handle.

Disco

Disco's error handling is rather simple, paired with its Plugins system you define a method called with on_event() that is called with the error that happened.

There big downside here is that you have no access to anything about the event that raised this error.

API Definition

After considering other API wrapper's event handling behaviour let's take a look at what the final API for Wumpy will be.

Invocation and Introspection

Error handlers are very similar to event handlers, but the systems will be kept separate. This is so that commands and event listeners don't allow just about any event to be registered (which won't ever be dispatched either).

Error handlers are called with two arguments, the former is the event or interaction used to dispatch the callback and the latter is the Exception subclass that was raised.

This means that the second argument will be introspected to see what subclass the handler wants to handle.

Error Handler Order

Compared to other wrappers error handlers will be called until the error has been considered handled.

This means that for the best experience error handlers should be called in the reverse order of how "broad" they are considered. For example, an error handler annotated to handle RuntimeError should be called before one annotated to handle Exception.

For that reason error handlers are sorted (and called) in the reversed order of the length of the annotation's __mro__. See the following code:

l = [LookupError, KeyError, Exception]
l.sort(key=lambda elem: len(elem.__mro__), reverse=True)

print(l)  # [<class 'KeyError'>, <class 'LookupError'>, <class 'Exception'>]

There's an issue with this though, the order of the final list can depend on the input list. For example sorting [NameError, LookupError] compared to [LookupError, NameError] has different results (they both have an __mro__ length of 4).

The easiest way to fix this - and make the algorithm determinable no matter what the order of the input list is - is to first sort the list by the name of the error, making the final code look like this:

l = [...]  # List of Exception subclasses
l.sort(key=str)
l.sort(key=lambda elem: len(elem.__mro__), reverse=True)

Marking Errors Handled

As previously mentioned error handlers are called until they are considered handled - this is done by returning a boolean.

True is returned to indicate that the error was handled, False can be returned to indicate that the error should not propagate. None will not be an acceptable value

Using this system, here is an example that looks at an attribute:

async def handle_error(event, exception: CustomException):
    if getattr(exception, 'should_handle', False):
        return False

    ...  # Whatever you want to do when experiencing this error

    # Tell Wumpy that the error has been handled.
    return True