2020 twenty-four merry days of Perl Feed

There is No Try

Try::Tiny - 2020-12-03

2020 has been time consuming - a global pandemic, giant fires, horrific floods and political unrest - which has left us little time for side projects. This year we're looking back to happier times into the 20+ year archive with the Best of the Perl Advent Calendar.

eval Stinks

This is a pretty common pattern of code:

my $result = eval { $self->computation_helper->compute_stuff };

if ($@) {
  $self->set_last_error( "couldn't get stuff computed: $@" );
  return undef;
}

return $result;

In other words, try to call some method on $self's helper object. If it fails, set an error and return undef. Otherwise, return the result we got. You can find code like this all over the place, and unfortunately, it's got problems.

The most horrible, and possibly well known, is that $@ being set isn't the right way to check for eval failing. (Let's not worry about the pathological case of someone throwing a false exception object. That's just sick.)

The real reason is that until recently, $@ could get easily clobbered by action at a distance. For example, look at the code again:

my $result = eval { $self->computation_helper->compute_stuff };

It's calling computation_helper which might look like this:

sub computation_helper {
  my ($self) = @_;
  return Computation::Helper->new( helper_for => $self );
}

It sets up some helper object that we can throw away when we're done. We call its compute_stuff method, which dies. At this point, eval starts to return false, with $@ set to that exception message. Unfortunately, little known to us, this code exists:

package Computation::Helper;
sub DESTROY {
  my ($self) = @_;
  eval { ... };
}

...and it's going to clobber our $@ when the helper object gets destroyed – and that's going to happen once the eval block is done and the helper object is no longer referenced by anything. Instead of testing for $@, we should test for a false return from eval:

my $result = eval { $self->computation_helper->compute_stuff };

if (! $result) {
# we died! set last_error, etc.
}

This isn't any good, either, though. What if compute_stuff can actually return false, or more specifically the empty string? We need to rewrite to force eval's hand:

my $result;
my $ok = eval { $result = $self->computation_helper->compute_stuff; 1 };

if (! $ok) {
# we died! set last_error, etc.
}

Now we know that eval will always return 1 unless it fails. This means we need to move the assignment to $result inside the eval, and we need to move the declaration to an new, earlier statement.

Finally, to keep our eval from clobbering anybody else's $@ in the future, we need to localize. In the end, our code ends up as:

my $result;
my $ok = do {
  local $@;
  eval { $result = $self->computation_helper->compute_stuff; 1 };
};

if (! $ok) {
  my $error = $@ eq '' ? 'unknown error' : "$@";
  $self->set_last_error( "couldn't get stuff computed: $error" );
  return undef;
}

return $result;

Even if not a huge increase in the code needed for the operation, it's a bunch of magic to remember every time. If you don't do this sort of thing, though, you've got a big opening for horrible error-handling problems.

Two Solutions

It's worth noting that these problems are greatly lessened in perl 5.14, where our hypothetical DESTROY method would not have clobbered the outer $@. If you can use 5.14, you should, and this is one very good reason.

That still leaves a bunch of boilerplate, though. This is why Try::Tiny has become so popular. Unlike many of the other, more feature-rich try/catch systems on the CPAN, Try::Tiny focuses on doing just the minimum needed to avoid the boilerplate above. For example, we'd write the code above as:

use Try::Tiny;

return try {
  $self->computation_helper->compute_stuff;
} catch {
  my $error = $_ eq '' ? 'unknown error' : "$_";
  $self->set_last_error( "couldn't get stuff computed: $error" );
  return undef;
};

It encapsulates the localization of the error variable and the addition of the constant true value to check for. The catch block is only entered if the try block died, and the exception thrown is in $_ (and $_[0]). Simple!

Extra Sugar

Try::Tiny also provides one more helper: finally. It lets you provide a block (or many blocks) of code to be run after the try and catch, no matter whether there was an error:

use Try::Tiny;

my $start = Time::HiRes::time;

return try {
  $self->computation_helper->compute_stuff;
} catch {
  my $error = $_ eq '' ? 'unknown error' : "$_";
  $self->set_last_error( "couldn't get stuff computed: $error" );
  return undef;
} finally {
  my $failed = @_;
  my $end = Time::HiRes::time;
  warn sprintf "took %f seconds to %s",
    $end - $start,
    $failed ? 'fail' : 'succeed';
};

You can tell whether the try block failed by whether there are any elements in @_. Even if somehow the contents of $@ couldn't be preserved, you'll find an undef in the argument list if the try failed. Otherwise, it will be empty. You can supply as many try blocks as you want.

Try::Tiny doesn't do much, but it nicely packages up a pattern that's important to use and really boring to type out every time. Not only does it save you from having to remember the details each time, but it gives you an interface that's easier to write and skim, too. Use it!

See Also

Gravatar Image This article contributed by: Mark Fowler <mark@twoshortplanks.com>