Is it possible to handle custom exceptions or exceptions other than Exception
risen on a different server / client / uplink environment?
For example a server module to handle an exception risen on an uplink module or viceversa?
Is it possible to handle custom exceptions or exceptions other than Exception
risen on a different server / client / uplink environment?
For example a server module to handle an exception risen on an uplink module or viceversa?
I got around this issue by doing something like this:
First, I create a custom Exception
on the server side that looks like this:
class MyException(Exception):
def __str__(self):
return "<{}> {}".format(type(self).__name__, " ".join(self.args))
This means that when __str__
is called to represent the Exception
, it will return a string that contains the name of the custom Exception
class as well as any messages passed to it as an argument, so:
>>> print(MyException('Hi!'))
<MyException> Hi!
>>>
In that case, on the client side, your handler would look like this:
try:
foo() # Any routine that can throw the custom Exception.
except Exception as e:
if '<MyException>' in str(e):
print('Caught it')
else:
raise e
You catch every exception that comes across on the client side, but you only handle them if their string matches your known tags, otherwise, you raise them again. Maybe not 100% perfect, but it seems to work so far.
Yes, there is an internal interface for this. We weren’t 100% sure about the API, so it’s not autocompleted or documented yet, but it’s a totally legitimate need, so here’s a beta release for you!
Make a Module (that’s code you can import from client and server), and define an exception class that inherits from anvil.server.WrappedError
. Then call anvil.server_register_exception_type()
and give it a fully qualified name (to distinguish it from any other class with the same name).
import anvil.server
class MyError(anvil.server.AnvilWrappedError):
pass
anvil.server._register_exception_type('my_module.MyException', MyError)
Now, if you raise this error on the server…
import my_module
@anvil.server.callable
def my_function():
raise MyError("Oops")
…you can catch it from the client:
try:
anvil.server.call('my_function')
catch my_module.MyError as e:
print("Yikes!")
(NB you’ll need the latest version of the Uplink to throw these exceptions: pip install --upgrade anvil-uplink
. This will work for you already if you’re using the Full Python runtime; it will arrive in the next few hours for Restricted users.)
(NB #2: We will be documenting this as a public interface, but for now I’m going to leave it here in beta and wait for feedback )
So far the beta’s working fairly well, but I am getting what may be a small bug. If I raise an exception on the server side with one argument, everything works well; however, if I attempt to raise the same exception with no arguments (i.e. raise ExMod.MyException()
), I get the following traceback:
TypeError: __init__() missing 1 required positional argument: 'error_obj'
at ServerModule1, line 6
called from Form1, line 17
called from Form1, line 17
Interestingly enough, if I put in ''
as a throwaway argument when I raise the same exception, the printed result using print(e)
in the catch is:
MyException: [unexpected error] on line 6
Similarly, in trying to pass two arguments as you might with raise Exception('1', '2')
in straight Python, I get the following traceback:
TypeError: __init__() takes 2 positional arguments but 3 were given
at ServerModule1, line 6
called from Form1, line 17
called from Form1, line 17
So for my feedback: even while in beta, it is a definite step forward for error handling across the client/server divide. Right now with the beta release the arguments aren’t quite as dynamic as a traditional Python Exception()
statement without *args
, and perhaps they don’t need to be. As Anvil grows, it would be nice for this functionality to behave more like a traditional Exception()
for a more seamless experience, but for now, it achieves the intended result.
I was wondering how this might be implemented with an Uplink server that may not have access to the original module though. Would you need to create a copy of the module on the local drive so the Uplink script has access to an appropriately named module and class, or is the registered name of the Exception
the most important piece regardless of the actual class you create? On a similar note, might there be any conflicts when multiple scripts register Exception
s under the same name? These questions are probably quite premature, but I wanted to get them down somewhere before I forgot.
Thanks for the answer @meredydd and for your continuing hard work!
Yes, that’s right - the Anvil client/server exception system assumes that all exceptions have a single string in their args
. Exceptions do not have the full range of expressiveness found in normal client-server communications.
If a server call throws an exception type you haven’t registered, you’ll just get an undifferentiated AnvilWrappedError
. (This is true on the Uplink, same as on the server or client. As you correctly guess, it’s the name passed to _register_exception_type()
that’s used to match them up.) Slightly awkward corners like this are why the function name still starts with an underscore
This is a useful mechanism. Could this be documented please?
I often end up with an exceptions.py
module that has whatever subclasses of AnvilWrappedError
I need plus the lines to register those.
The registration lines get a bit ugly and repetitive, so I use a decorator:
from anvil.server import AnvilWrappedError, _register_exception_type
class NamedError(AnvilWrappedError):
"""A base class for custom error classes
In order to register a custom error class, a name is required. This base class
ensures that a 'name' class attribute exists with a default value for use by
portable_exception.
It can be overridden in any subclass that requires a customised name.
"""
name = None
def portable_exception(cls):
"""A decorator to register a class as an exception"""
try:
name = cls.name or f"{__name__}.{cls.__name__}"
except AttributeError:
raise ValueError("Class to register must have a 'name' attribute")
_register_exception_type(name, cls)
return cls
@portable_exception
class MyError(NamedError):
pass
@portable_exception
class MyOtherError(NamedError):
pass
Might I suggest something similar as the API?
What is the latest guidance for this kind of situation? I assume this likely changed sometime in the last almost 5 years!
For context, I created a custom error in my client code that subclasses from AnvilWrappedError and is also decorated as a portable class. Yet, when I raise it from the server, the client is unable to catch it in try/except blocks; the error immediately bubbles right up and halts execution. I thought that this was supposed to enable this kind of flow?
Form
def button_1_click(self, **event_args):
"""This method is called when the button is clicked"""
try:
anvil.server.call('test_role_decorator')
print('success')
except AccessRequiredError as exc:
print('failure')
print(exc)
Client Module
class AccessRequiredError(anvil.server.AnvilWrappedError):
pass
@anvil.server.portable_class
class RoleRequiredError(AccessRequiredError):
def __init__(self, missing_roles):
super().__init__(f'User is lacking required role(s): {missing_roles}')
@anvil.server.portable_class
class PermissionRequiredError(AccessRequiredError):
def __init__(self, missing_permissions):
super().__init__(f'User is lacking required permission(s): {missing_permissions}')
Server Module
def roles_required(required_roles):
required_roles = set(required_roles)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_roles = get_user_roles()
missing_roles = required_roles - user_roles
if not missing_roles:
return func(*args)
else:
raise RoleRequiredError(missing_roles)
return wrapper
return decorator
@anvil.server.callable
@roles_required(['ADMIN'])
def test_decorator():
return True
Is there documentation now for this since the beta is out in production? Seems like a pretty important aspect of development.
Bump! I’m still unable to catch RoleRequiredError
s; I have to generically catch all Exception
s which sucks.
Have you tried taking a look at @owen.campbell s code above? I know it’s not official documentation, but it seems to be a method to explicitly register custom exceptions as portable, apart from just normal portable classes.
I haven’t used any of this, I just reread the thread above where Meredydd said they might revisit it in the future, and then Owen posted his method that works, despite the official docs not existing.
If you have, maybe start a new thread with what in his code no longer works (or doesn’t fit what you are trying to do)
Right. You have to use anvil.server._register_exception_type
as described above. I’ve used Owen’s code and it works reliably with the flow you’re wanting, @joshelbahrawy.
Alas, it hasn’t changed, as far as I can tell. +1 for documenting it, please.
Thanks Owen! I’ve been bashing my head against the wall trying to understand how to handle exceptions here and your snippet works like a charm. Thanks for contributing!
I’ve used Owen’s code and it works like a charm.