Thursday, November 5, 2009

Perl error handling

[Original spanish article]

Exception handling in Perl is a bit different than we are probably used to, particularly Perl has no try/catch/throw as some other languages, but that doesn't mean that it can't do exception handling, Perl can catch and handle exceptions as well as any other language but it has a slightly different structure.

Exception handling in Perl is based on the use of the eval operator, which allows the evaluation of code and error catching, when eval receives a string, it compiles the code inside it and executes it, however any error that happens in the code, from the compilation to execution would abort the only the eval while our program will continue its execution, for example:

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 say "El resultado es: $result";

Although the program works, the result of eval is undef, because division by zero prevented the return of any value, this also causes a warning on line 3 about the use of an uninitialized value.

What we need to know is whether the eval was successful or not, and that information is in the special variable $@ (also known as $EVAL_ERROR if we use the module English).

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 if ( $@ ) {
4     say "Ooops: $@";
5 }
6 else {
7     say "El resultado es: $result";
8 }

The problem with this solution is that the code within the string is not checked at compile time, because it is compiled at run time, and although this is extremely powerful, in most cases we are just interested in the eval's ability to catch errors, the second form of eval, takes a block of code that is checked during compilation of the program, and we can use it like this:

2 my $result = eval { 5 / 0 };

In this form of eval, the braces ({}) mark the catch block where exception handling is required and returns the last expression of this block, or undef if an error occurs while executing it (because it has already been compiled altogether with the containing program).

The last primitive we need to complete Perl's exception system is die, which allows to throw an exception, this routine receives a value that is assigned to the variable $@, so we could make a program that throws an exception like this:

 1 use Modern::Perl;
 2 use IO::File;
 3 
 4 eval {
 5     my $fh = IO::File->new("AlgunArchivo.txt", "r");
 6     die("No se puede abrir") unless $fh;
 7 };
 8 if ( $@ ) {
 9     say "Ooops: $@";
10 }

Some people may think that this way of capturing exceptions is archaic, however, it is as good as any other, and with the facilities of Perl could be used as basis for implementing a structure similar to that of other languages, something like try/catch. As I've already said on other articles Perl is an excellent language for implementing new features based on the language primitives, and I will roll my own version of try/catch just for fun:

 1 use Modern::Perl;
 2 use IO::File;
 3
 4 sub try(&) {
 5     eval { shift->() };
 6 }
 7
 8 sub catch(&) {
 9     if ( $@ ) {
10         local $_ = $@;
11         shift->();
12     }
13 }
14
15 try {
16     my $fh = IO::File->new( "AlgunArchivo.txt", "r" );
17     die("No se puede abrir") unless $fh;
18 };
19 catch {
20     say "Ooops: $_";
21 };


Here the Perl prototype (&) allows subroutines try and catch to receive a closure, but the prototype will allow to remove the sub declaration, pretending that try and catch are control structures with an associated code block, while they are just plain subroutines whose first parameter is a closure, and thus can be invoked, at line 5 the first argument is removed (with shift) and used to execute the closure (with ->()) within the eval, so any exception inside the closure code will abort the eval and exit the try subroutine.

When used after a try, catch localizes any value of $@ in $_ and runs the closure, which can use $_ as the value of the exception.

To make an extension that allows to use the newly created structures, we just make a new module, I will call MyTryCatch and should be in the file "MyTryCatch.pm":


 1 package MyTryCatch;
 2
 3 use Exporter;
 4
 5 our $VERSION = "1.000";
 6 our @EXPORT_OK = qw( try catch );
 7 our @EXPORT = @EXPORT_OK;
 8
 9 sub try(&) {
10     eval { shift->() };
11 }
12
13 sub catch(&) {
14     if ( $@ ) {
15         local $_ = $@;
16         shift->();
17     }
18 }

19

20 1;

Thus we may use the new structure in any program easily:

 1 use Modern::Perl;
 2 use IO::File;
 3 use MyTryCatch;
 4 
 5 try {
 6     my $fh = IO::File->new( "AlgunArchivo.txt", "r" );
 7     die("No se puede abrir") unless $fh;
 8 };
 9 catch {
10     say "Ooops: $_";
11 };

The primitives just created have some defects, for example the allow the use of a catch without a catch, a return statement within a try or catch block will exit the block and not the enclosing subroutine, among others. However with a bit more effort we could make an extension that declares a structure that behaves better.

There are several CPAN modules that let to do exceptions handling  from the simplest Try::Tiny, who suffers from some drawbacks of MyTryCatch to the most complex TryCatch that uses deep magic from Devel::Declare to make an exception handling structure with almost anything you can imagine.

If your requirements are not demanding my recommendation is to use Try::Tiny, it is tiny, has almost no dependencies and is easy to install, on the other hand if you want an exception handling system that does everything, you do not mind much about resource consumption and have the patience to install dozens of modules, you can use TryCatch.

2 comments:

  1. There was Try::Tiny module for that

    ReplyDelete
  2. There are two major problems with this toy implementation:

    1. the invocation of the catch block is unconditional, but $@ is not guaranteed to have a sensible value even if an error was thrown, so this can contribute to false negatives (undetected errors)

    2. $@ is not localized

    These issues are discussed in the Try::Tiny documentation, but I think it's worth mentioning again in this context

    ReplyDelete