DEV Community

loading...

Exceptional Perl: Failure is an option

mjgardner profile image Mark Gardner Originally published at phoenixtrap.com on ・6 min read

Failure is a universal truth of computers. Files fail to open, web pages fail to load, programs fail to install, messages fail to arrive. As a developer you have no choice but to work in a seemingly hostile environment in which bugs and errors lurk around every corner.

Hopefully you find and fix the bugs during development and testing, but even with all bugs squashed exceptional conditions can occur. It’s your job as a Perl developer to use the tools available to you to handle these exceptions. Here are a few of them.

eval, die and $EVAL_ERROR ($@) (updated)

Perl has a primitive but effective mechanism for running code that may fail called eval. It runs either a string or block of Perl code, trapping any errors so that the enclosing program doesn’t crash. It’s your job then to ignore or handle the error; eval will return undef (or an empty list in list context) and set the magic variable $@ to the error string. (You can spell that $EVAL_ERROR if you use the English module, which you probably should to allow for more readable code.) Here’s a contrived example:

use English;

eval { $foo / 0; 1 }
  or warn "tried to divide by zero: $EVAL_ERROR";
Enter fullscreen mode Exit fullscreen mode

(Why the 1 at the end of the block? It forces the eval to return true if it succeeds; the or condition is executed if it returns false.)

What if you want to purposefully cause an exception, so that an enclosing eval (possibly several layers up) can handle it? You use die:

use English;

eval { process_file('foo.txt'); 1 }
  or warn "couldn't process file: $EVAL_ERROR";

sub process_file {
    my $file = shift;
    open my $fh, '<', $file
      or die "couldn't read $file: $OS_ERROR";

    ... # do something with $fh
}
Enter fullscreen mode Exit fullscreen mode

It’s worth repeating that as a statement: You use exceptions so that enclosing code can decide how to handle the error. Contrast this with simply handling a function’s return value at the time it’s executed: except in the simplest of scripts, that part of the code likely has no idea what the error means to the rest of the application or how to best handle the problem.

autodie (updated)

Since many of Perl’s built-in functions (like open) return false or other values on failure, it can be tedious and error-prone to make sure that all of them report problems as exceptions. Enter autodie, which will helpfully replace the functions you choose with equivalents that throw exceptions. Introduced in Perl 5.10.1, it only affects the enclosing code block, and even goes so far as to set $EVAL_ERROR to an object that can be queried for more detail. Here’s an example:

use English;
use autodie; # defaults to everything but system and exec

eval { open my $fh, '<', 'foo.txt'; 1 } or do {
    if ($EVAL_ERROR
      and $EVAL_ERROR->isa('autodie::exception') {
        warn 'Error from open'
          if $EVAL_ERROR->matches('open');
        warn 'I/O error'
          if $EVAL_ERROR->matches(':io');
    }
    elsif ($EVAL_ERROR) {
        warn "Something else went wrong: $EVAL_ERROR";
    }
};
Enter fullscreen mode Exit fullscreen mode

try and catch

If you’re familiar with other programming languages, you’re probably looking for syntax like try and catch for your exception needs. The good news is that it’s coming in Perl 5.34 thanks to the ever-productive Paul “LeoNerd” Evans; the better news is that you can use it today with his Feature::Compat::Try module, itself a distillation of his popular Syntax::Keyword::Try. Here’s an example:

use English;
use autodie;
use Feature::Compat::Try;

sub foo {
    try {
        attempt_a_thing();
        return 'success!';
    }
    catch ($exception) {
        return "failure: $exception"
          if not $exception->isa('autodie::exception');

        return 'failed in ' . $exception->function
          . ' line ' . $exception->line
          . ' called with '
          . join ', ', @{$exception->args};
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that autodie and Feature::Compat::Try are complementary and can be used together; also note that unlike an eval block, you can return from the enclosing function in a try block.

The underlying Syntax::Keyword::Try module has even more options like a finally block and a couple experimental features. I now prefer it to other modules that implement try/catch syntax like Try::Tiny and TryCatch (even though we use Try::Tiny at work). If all you need is the basic syntax above, using Feature::Compat::Try will get you used to the semantics that are coming in the next version of Perl.

Other exception modules (updated)

autodie is nice, and some other modules and frameworks implement their own exception classes, but what if you want some help defining your own? After all, an error string can only convey so much information, may be difficult to parse, and may need to change as business requirements change.

Although CPAN has the popular Exception::Class module, its author Dave Rolsky recommends that you use Throwable if you're using Moose or Moo. If you’re rolling your own objects, use Throwable::Error.

Using Throwable couldn’t be simpler:

package Foo;

use Moo;
with 'Throwable';

has message => (is => 'ro');

... # later...

package main; 
Foo->throw( {message => 'something went wrong'} );
Enter fullscreen mode Exit fullscreen mode

And it comes with Throwable::Error, which you can subclass to get several useful methods:

package Local::My::Error;
use parent 'Throwable::Error';

... # later...

package main;
use Feature::Compat::Try;

try {
    Local::My::Error->throw('something bad');
}
catch ($exception) {
    warn $exception->stack_trace->as_string;
}
Enter fullscreen mode Exit fullscreen mode

(That stack_trace attribute comes courtesy of the StackTrace::Auto role composed into Throwable::Error. Moo and Moose users should simply compose it into their classes to get it.)

Testing exceptions with Test::Exception

Inevitably bugs will creep in to your code, and automated tests are one of the main weapons in a developer’s arsenal against them. Use Test::Exception when writing tests against code that emits exceptions to see whether it behaves as expected:

use English;
use Test::More;
use Test::Exception;

...

throws_ok(sub { $foo->method(42) }, qr/error 42/,
  'method throws an error when it gets 42');
throws_ok(sub { $foo->method(57) }, 'My::Exception::Class',
  'method throws the right exception class');

dies_ok(sub { $bar->method() },
  'method died, no params');

lives_and(sub { is($baz->method(17), 17) },
  'method ran without exception, returned right value'); 

throws_ok(sub { $qux->process('nonexistent_file.txt') },
  'autodie::exception', # hey look, it's autodie again
  'got an autodie exception',
);
my $exception = $EVAL_ERROR;
SKIP: {
    skip 'no autodie exception thrown', 1
      unless $exception
      and $exception->isa('autodie::exception');
    ok($exception->match(':socket'),
      'was a socket error:' . $exception->errno);
}

done_testing();
Enter fullscreen mode Exit fullscreen mode

Note that Test::Exception’s functions don’t mess with $EVAL_ERROR, so you’re free to check its value right after you call it.

Documenting errors and exceptions

If I can leave you with one message, it’s this: Please document every error and exception your code produces, preferably in a place and language that the end-user can understand. The DIAGNOSTICS section of your documentation (you are writing documentation, right, not just code comments?) is a great candidate. You can model this section after the perldiag manual page, which goes into great detail about many of the error messages generated by Perl itself.

(A previous version of this article did not note that one should make sure a successful eval returns true, and incorrectly stated that Class::Exception and Throwable were deprecated due to a bug in the MetaCPAN web site. Thanks to Dan Book for the corrections.)

Discussion (2)

pic
Editor guide
Collapse
thibaultduponchelle profile image
Tib

Great article as usual, thank you for having switched on the light a bit more on exceptions for me 😃

Collapse
mjgardner profile image
Mark Gardner Author

Thanks! I learned about autodie's exception objects while writing this, so it helps me too!