-
Notifications
You must be signed in to change notification settings - Fork 1
Error Handling
This is separate document - geared to developers interested in the library's design choices - documentating the error handling API.
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.
Let's start like was done for the dispatch and extension API by looking at other wrappers and compare pros and cons.
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 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'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.
After considering other API wrapper's event handling behaviour let's take a look at what the final API for Wumpy will be.
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.
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)
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