Handling exception coming from the server/uplink modules

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?

1 Like

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.

2 Likes

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 :slight_smile: )

3 Likes

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 Exceptions under the same name? These questions are probably quite premature, but I wanted to get them down somewhere before I forgot. :slight_smile:

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 :slight_smile:

1 Like

This is a useful mechanism. Could this be documented please?

1 Like

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?

4 Likes