Deferred Failures

A Deferred is an object that represents a promise that it will eventually become something else. Either it will be called back, and its eventual result will be the final value returned to its callback, or it will fail and become an instance of the Failure class.

The failure class helps to circumvent some of the inconveniences that standard Python error mechanisms cause for asynchronous programs. An instance of the Failure class contains frames, which provide information about the file, function, and line that an error occurred, and the Failure instance can collect multiple frames, eventually finishing its errback chain with a stack containing one or more frames, each of which describes an error.

Failure.value()

The exception instance responsible for causing a failure can be checked by calling Failure.value() on a Failure instance. This is useful for quickly identifying the problem in situations where an initial exception has caused cascading errors.

How to use it

Below, we create a function assessFailValue(d) which examines the value variable of the Failure to determine what to do next. We then instantiate a Deferred with an errback and callback and then we cancel it, passing it to assessFailValue(d).

assessFailValue(d) examines the Failure instance that our Deferred has become and determines whether or not the first exception that was raised is one that should be handled further.

Because we are assuming in our example that a cancelled Deferred was cancelled by the user, and is therefore not really a bad thing, and may be something we want to treat like a callback, we can actually chain our errback into a callback with the value “Eventual success”.

This is passed back up the callback chain and ‘d’ finishes by firing its original callback and printing “Eventual Success”. If this last bit was a little confusing, don’t worry, we’re going to learn more about it in the next tutorial.

class ExampleTests(unittest.TestCase):

    def testFailureValue(self):

        # Set up a function to examine our Failure
        def assessFailValue(d) -> Deferred:

            # If the first exception is a CancelledError, a user cancelled it
            if type(d.value) == CancelledError:

                # So we don't consider it an error past this point
                return succeed("Eventual Success!")
            else:

                # /continue to handle the error elsewhere/
                return fail(1)

        #create a deferred that we will fail
        d = Deferred()
        d.addErrback(assessFailValue)
        d.addCallback(print)

        # Pretend a user cancelled our Deferred, causing it to errback
        d.cancel()
        return d

Which should output something like:

Ran 1 test in 0.137s

OK

Process finished with exit code 0
Eventual Success!

Failure.check()

Failures are useful for handling errbacks. For example, if a server uses a Deferred to make a request that eventually times out, the Deferred can errback with a TimeoutError and a CancelledError and it can probably be safely ignored. However, if that same Deferred raises a TypeError it could be an indication that something more important has gone wrong.

A convenient way to filter out the less important errors is the Failure.check() function, which checks to see if a failure instance is in a predetermined list. If the failure is in the list, it returns the match, otherwise it returns None. This makes it easy to create a rule that would ignore a TimeoutError and log or notify on a TypeError.

How to use it

Below, much like in the first example, we check to see if the failure was raised by an exception that we should be concerned about, but this time we do it using the Failure class’s inbuilt .check() function, which returns either None, or the kind of exception thrown, allowing us to easily check a list of exceptions (a number of exceptions passed to the function as an argument, not a list data structure). As in the first example, because we raised a CancelledError, the exception was found and returned as a callback:

def testFailureCheck(self):

    # Set up a function to examine our Failure
    def assessFailCheck(d) -> Deferred:


        # If the first exception is one we are ok with
        if d.check(TimeoutError, CancelledError, 1) != None:

            # So we don't consider it an error past this point
            return succeed("Eventual Success!")
        else:
            # /continue to handle the error elsewhere/

            return fail(1)

    #create a deferred that we will fail
    d = Deferred()
    d.addErrback(assessFailCheck)
    d.addCallback(print)

    # Pretend a user cancelled our Deferred, causing it to errback
    d.cancel()
    return d