2020/12/05

2020 Perl Advent Calendar - Day 5

<< First | < Prev | Next >

Yesterday we took a look at how failure handling works when using async/await syntax. We saw the close association between Perl's exception throwing, and future instances in failed state. In forming this association, however, we did gloss over one detail. Futures can store an entire list of values in both a successful and failed state. When representing a failure, the first item in this list contains the message, but additional values are also permitted to provide more information on context.

In the original blog post series, day 7 talked about how to handle failed futures with these lists of values. When using async/await syntax and simply using die to throw an exception there's no obvious place to put extra values. For this problem we can use an instance of Future::Exception. This class is specifically designed for this problem; containing the extra values while appearing like a normal exception object for the purposes of die and catch.

For example, perhaps we want to check a GET request to ensure it responds with a 2xx or 3xx response code, and if not throw an exception in the http category, which also includes the received response object itself, in case the caller wants to inspect it.

use feature 'signatures';
use Future::AsyncAwait;
use Future::Exception;

async sub GET_checked($url)
{
    my $response = GET($url);

    if($response->code =~ m/^[23]/) {
        Future::Exception->throw("Unable to fetch $url",
            http => $response
        );
    }
    ...
}

Over the past 7 years since the original article was written, a pattern for using failures with values has been established. The first positional value should be a short string, describing the kind of failure, or in some way explaining why and when it happened, to give a clue to how to decode the other values. This "category name" can be used by exception dispatch logic when responding to the particular kind of failure, by inspecting the ->category method of a caught Future::Exception instance.

The original article on day 8 showed a combined success-or-failure handling using a two-argument ->then sequencing method on a future. There isn't a direct parallel using async/await and try/catch, but instead it becomes just a standard code shape for writing exception flow logic.

use Future::AsyncAwait;
use Syntax::Keyword::Try;

my $response;
try {
    $response = await GET("http://my-site-here.com/news");
}
catch($e isa Future::Exception) {
    if($e->category eq "http") {
        my ($response) = $e->details;
        
        return "(no title)" if $response->code == 204;
    }
    die $e;
}

return get_page_title($response->content);

The article for day 9 introduced another future construction methods, ->call, for ensuring that any exception thrown by a piece of code is always turned into a future in failed state. This isn't necessary when using async sub, because any exception thrown by that function immediately becomes a failed future, even if encountered very early on. This is ensured by the way that async sub works internally.

use Future::AsyncAwait;

async sub get_article($id_num)
{
    $id_num > 0 or die "Invalid ID number $id_num";
    return await GET("http://my-site-here.com/article-$id_num");
}

my $f = get_article(-123);

print "F is a failed future\n" if $f->is_failure;

Here when we call get_article with a bad argument, the future it returns is already ready, and in a failed state. The call itself does not throw an exception - remember, we only get that by applying such a failed future to the await keyword.

<< First | < Prev | Next >

No comments:

Post a Comment