Trying for a Happy Christmas
Every year around this time Santa has some presents to deliver. And every year, try as he might, sometimes things don't go very well. There's always someone whose chimney is just too small for him to fit down, or the fire's still lit, or maybe even someone doesn't have a chimney so he'll have to come in the door like everybody else does.
If Santa is going to succeed at delivering as many presents as possible, he can't just stop at the first failure. He'll have to carry on past those, making as best an effort as possible.
In Perl, there's a number of ways we can handle failed attempts to call a function. We could use eval
, though this has a number of non-ideal properties and can lead to code that doesn't read very well. Better is to use one of the CPAN modules that wrap this in some nicer syntax.
One nice module for doing this with is Syntax::Keyword::Try, which provides a neat syntax similar to that used by a number of other languages, being marked by two new keywords try
and catch
:
use Syntax::Keyword::Try 'try';
sub attempt_delivery
{
my ( $present ) = @_;
try {
deliver_via_chimney( $present );
}
catch {
print "We couldn't deliver it because $@";
}
}
attempt_delivery( $_->present ) for
grep { not $_->is_naughty } @children;
As compared to simple eval
syntax and checking the value of $@
afterwards, we can see this looks a lot neater. Instead of looking at the truth of $@
(which already is a buggy antipattern), or testing the truth of the return value of eval
itself, we simply use the catch
keyword to provide the code to handle a failure. Because it's using the syntax plugin system, the keyword already acts like a full statement and not an expression, so no semicolon is needed at the end of it.
So far our error handling hasn't been very good though, because all we did was print that a failure happened. Perhaps we can do better. If Santa can't deliver the present through the chimney, he'll just have to come in the door instead.
try {
deliver_via_chimney( $present );
}
catch {
try {
deliver_via_door( $present );
}
catch {
print "We couldn't deliver it at all, because $@";
}
}
A neater way to write this code, and more extensible in case we find even more ways to deliver presents, is to use a return
statement inside a try
block. This is another useful ability that Syntax::Keyword::Try
has that regular eval
does not.
my $failure;
try {
deliver_via_chimney();
return;
}
catch { $failure //= $@ }
try {
deliver_via_door();
return;
}
catch { $failure //= $@ }
...
print "We couldn't deliver at all, because $failure";
We also now have the advantage that it's now the first failure message that we print at the end. If one of the later attempts succeeds, it doesn't really matter any more what the earlier failure was.
Well, to a point. It's not particularly nice to ignore any possible error, because it could have been something unrelated - an unexpected type of data passed in, a missing module dependency, all sorts of things.
Some languages have typed exceptions, but in Perl we generally make do with string messages and testing them with regexps. If we die
an exception from a catch block it re-throws it, effectively acting like we didn't catch it in the first place.
sub attempt_delivery
{
try {
deliver_via_chimney();
return;
}
catch {
die $@ if $@ !~ m/^Cannot fit in the chimney/;
}
...
}
Comparing against other try / catch modules
As we previously mentioned there is more than one way to do error handling on the CPAN. Let's see how Syntax::Keyword::Try holds up against the possible solutions.
Comparing Syntax
First let's look at an example written with each of the techniques. We've got a subroutine that refills the sack Santa's carrying with presents from the main store on the sleigh. We want our subroutine to return immediately if there's already more than ten things in Santa's sack, and it's possible that counting the items might throw an exception if Santa's not using a sack this trip (sometimes he just throws a bike over his shoulder.) We want to be careful to re-throw any error that isn't to do with Santa not using a sack.
- eval
-
The inbuilt eval syntax is the basis for exception handling, and being a native keyword is very quick. However, it can't return from the subroutine from within the eval block. The eval-as-statement-not-as-block instead of a try block and no dedicated catch syntax make it confusing. Worse still, you have to check the return value of the eval statement rather than checking $@ if you want to avoid potential bugs (on some versions of perl
$@
can be accidentally unset during custom object destruction which might happen between the time the error is thrown and you check for it.)sub refill_sack {
my $should_return;
if (eval {
$should_return = sack_item_count() > 10;
1;
}) {
return if $should_return;
} else {
return if $@ =~ /not using sack this trip/;
die;
}
...
}Writing it properly is verbose and error prone.
- Try::Tiny
-
Try::Tiny is a simple pure Perl solution that fairs very well on the CPAN. It's two main drawbacks is that it's very slow compared to the other techniques described here, and that it's not possible to return from the subroutine in either the try or catch blocks (as they're just syntactic sugar for anonymous subroutines.)
sub refill_sack {
my $should_return = 1;
try {
$should_return = sack_item_count() > 10;
} catch {
die $_ unless /not using sack this trip/;
};
return if $should_return;
...
}With its simplistic approach Try::Tiny also has a few oddities that niggle - it uses
$_
instead of$@
(meaning you can't just use a baredie
statement to re-throw the current error) and it requires that annoying semicolon at the end of the blocks. - TryCatch
-
TryCatch is a module based on Devel::Declare, which is module to subvert the Perl parser to allow new syntax. By swapping out the Perl parser with a custom parser when keywords are detected, and then calling the Perl parser back again to parse the code within the blocks, new try syntax is created.
sub refill_sack {
try {
return if sack_item_count() > 10;
} catch ($e where { /not using sack this trip/ }) {
return;
}
...
}TryCatch is the only module listed here that allows conditional checking of the return value with explicit syntax; We don't need to explicitly have to re-throw the unhanded error, if the regular expression doesn't match it'll automatically re-thrown for us.
The main problem with TryCatch is that the custom parser technology that it uses isn't considered as reliable as the pluggable keyword technology provided by modern perls that Syntax::Keyword::Try makes use of.
- Syntax::Keyword::Try
-
For completeness, here's the same example written for Syntax::Keyword::Try:
sub refill_sack {
try {
return if sack_item_count() < 10;
} catch {
return if $@ =~ /not using sack this trip/;
die;
}
...
}
Benchmarking
Some simple benchmarking shows that Try::Tiny is very slow, eval is fast, but the other two modules come in the same order of magnitude as eval.
Feature Comparison
Finally A proper comparison would be amiss without a feature comparison chart:
eval | Try::Tiny | TryCatch | Syntax::Keyword::Try | |
---|---|---|---|---|
Requires no dependencies | ✔ | ✗ | ✗ | ✗ |
Pure Perl solution | ✔ | ✔ | ✗ | ✗ |
Runs on perl 5.8 | ✔ | ✔ | ✔ | ✗ |
Runs on perl 5.14 | ✔ | ✔ | ✔ | ✔ |
No Devel::Declare parser swapout | ✔ | ✔ | ✗ | ✔ |
Addresses $@ accidentally cleared bug | ✗ | ✔ | ✔ | ✔ |
Supports try / catch like syntax | ✗ | ✔ | ✔ | ✔ |
Doesn't require semicolon after block | ✗ | ✗ | ✔ | ✔ |
Can 'return' from within block | ✗ | ✗ | ✔ | ✔ |
Can 'last' from within the block | ✗ | ✗ | ✗ | ✔ |
Allows rethrowing with no arg 'die' | ✔ | ✗ | ✔ | ✔ |
Maintance release in the last 3 years? | ✔ | ✔ | ✗ | ✔ |