Tuesday, September 1, 2009

Try::Tiny

I just released Try::Tiny, yet another try { } catch { } module.

The rationale behind this module is that Perl's eval builtin requires large amounts of intricate boilerplate in order to be used correctly. Here are the problems the boilerplate must address:

  • $@ should be localized to avoid clearing previous values if the eval succeeded.
  • This localization must be done carefully, or the errors you throw with die might also be clobbered.
  • if ( $@ ) { ... } is not guaranteed to detect errors.

The first problem causes action at a distance, so if you don't address it your code is very impolite to others. The other two problems reduce the reliability of your code. The documentation contains an in depth explanation of all of these issues.

Here is my standard boilerplate for a polite, defensive eval blocks:

my ( $error, $failed );

{
   local $@;

   $failed = not eval {

       ...;

       return 1;
   };

   $error = $@;
}

if ( $failed ) {
   warn "got error: $error";
}

This is extremely tedious when really all I want to do is protect against potential errors in the ... part.

Try::Tiny does that and little else; it runs on older Perls, it works with the various exception modules from the CPAN, it has no dependencies, it doesn't invent a new catch syntax, and it doesn't rely on any mind boggling internals hacks.

If you are not comfortable using TryCatch and you don't mind missing out on all of its awesome features then you should use Try::Tiny.

If you think plain eval { } is fine then your code is potentially harmful to others, so you should also use Try::Tiny.

Despite its minimalism Try::Tiny can be pretty expressive, since it integrates well with Perl 5.10's switch statement support:

use 5.010; # or use feature 'switch'

try {
    require Foo;
} catch {
    when ( /^Can't locate Foo\.pm in \@INC/ ) { } # ignore
    default { die $_ } # all other errors are fatal
};

For backwards compatibility you can obviously just inspect $_ manually, without using the when keyword.

11 comments:

Petar Shangov said...

Thanks, just what I needed!

taro-nishino said...

In the standard boilerplate, I don't think it matters whether you say accurately, but to be accuracy:

--- boilerplate.orig Tue Sep 1 17:22:55 2009
+++ boilerplate Tue Sep 1 17:24:01 2009
@@ -1,4 +1,4 @@
-my ( $failed, $failed );
+my ( $failed, $error );

{
local $@;



Taro Nishino

Anonymous said...

If this module is implemented the way I think it is, and judging by a quick glance at the source on CPAN, it is, then in the general case, you will need to append a semicolon after

try { ... }
catch { ... }

To us, it looks like a control structure, but to perl, it's just a single statement, with an extra function call for the catch, and 2 closures.

nothingmuch said...

Thanks, fixed my shoddy gists ;-)

Unknown said...

Thanks! Looks useful.

Anonymous said...

localizing $SIG{__DIE__} would be helpful

Unknown said...

# syntax error near "; }"
use 5.010;
use autouse 'Try::Tiny' => qw( try catch );

try { die 'tratata'; } catch { say 'msg from catch: '. $_; };


# msg from catch: tratata
use 5.010;
use Try::Tiny qw( try catch );

try { die 'tratata'; } catch { say 'msg from catch: '. $_; };

nothingmuch said...

The problem is that autouse doesn't know the prototypes for try/catch, so it doesn't know about the block syntax. { } is the anonymous hash constructor when found in a scalar value context, and a ; inside a hash constructor is a syntax error.

Honestly I don't see the point in autouse though, both autouse and Try::Tiny load in under 0.01s on my computer.

Anonymous said...

The movie: http://vimeo.com/6451409

Anonymous said...

I wonder whether localising $@, while polite, is something that one should really do.

If I'm careful to use eval properly wherever I use it--that is, retrieving the value of $@ immediately after my eval--then I'm not sure that localising $@ would assist me in any way.

As far as I can see, it would only serve to make other poorly-written eval code work as expected, which would in turn inhibit the correction of that code.

What are your thoughts on this?

nothingmuch said...

It's a hard to track down problem, and in CPAN modules you can't know where your code will be used, so I think it's a good practice to not stomp on global values.

The reason is that if an eval is introduced into a previously working code path without localization, previously working but obviously incorrect code would stop working.

The CPAN clients don't make it very easy to rollback a module upgrade (but see http://blog.woobling.org/2009/10/versioned-sitelib.html) so this can be a real pain to deal with when you're just bringing installed modules up to date (perhaps implicitly using a dependency).

The headaches involved and the fact that such code exists in the wild make localizing a good habit in my opinion, but yes, in an ideal world you wouldn't need to do that (though in an ideal perl would have a try/catch without action at a distance, and nobody would be programming in it anyway ;-)