Exceptional Perl: Failure is an option

Failure is a uni­ver­sal truth of com­put­ers. Files fail to open, web pages fail to load, pro­grams fail to install, mes­sages fail to arrive. As a devel­op­er you have no choice but to work in a seem­ing­ly hos­tile envi­ron­ment in which bugs and errors lurk around every corner.

Hopefully you find and fix the bugs dur­ing devel­op­ment and test­ing, but even with all bugs squashed excep­tion­al con­di­tions can occur. It’s your job as a Perl devel­op­er to use the tools avail­able to you to han­dle these excep­tions. Here are a few of them.

eval, dieand $EVAL_ERROR ($@) (updated)

Perl has a prim­i­tive but effec­tive mech­a­nism for run­ning code that may fail called eval. It runs either a string or block of Perl code, trap­ping any errors so that the enclos­ing pro­gram does­n’t crash. It’s your job then to ignore or han­dle the error; evalwill return undef(or an emp­ty list in list con­text) and set the mag­ic vari­able $@to the error string. (You can spell that $EVAL_ERRORif you usethe Englishmod­ule, which you prob­a­bly should to allow for more read­able code.) Here’s a con­trived example:

use English;

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

(Why the 1at the end of the block? It forces the evalto return true if it suc­ceeds; the orcon­di­tion is exe­cut­ed if it returns false.)

What if you want to pur­pose­ful­ly cause an excep­tion, so that an enclos­ing eval (pos­si­bly sev­er­al lay­ers up) can han­dle 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
}

It’s worth repeat­ing that as a state­ment: You use excep­tions so that enclos­ing code can decide how to han­dle the error.Contrast this with sim­ply han­dling a func­tion’s return val­ue at the time it’s exe­cut­ed: except in the sim­plest of scripts, that part of the code like­ly has no idea what the error means to the rest of the appli­ca­tion or how to best han­dle the problem.

autodie

Since many of Perl’s built-​in func­tions (like open) return false or oth­er val­ues on fail­ure, it can be tedious and error-​prone to make sure that all of them report prob­lems as excep­tions. Enter autodie, which will help­ful­ly replace the func­tions you choosewith equiv­a­lents that throw excep­tions. Introduced in Perl 5.10.1, it only affects the enclos­ing code block, and even goes so far as to set $EVAL_ERRORto an objectthat 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";
    }
};

tryand catch

If you’re famil­iar with oth­er pro­gram­ming lan­guages, you’re prob­a­bly look­ing for syn­tax like tryand catchfor your excep­tion needs. The good news is that it’s com­ing in Perl 5.34thanks to the ever-​productive Paul ​“LeoNerd” Evans; the bet­ter news is that you can use it today with his Feature::Compat::Trymod­ule, itself a dis­til­la­tion of his pop­u­lar 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};
    }
}

Note that autodieand Feature::Compat::Tryare com­ple­men­tary and can be used togeth­er; also note that unlike an evalblock, you can return from the enclos­ing func­tion in a tryblock.

The under­ly­ing Syntax::Keyword::Trymod­ule has even more options like a finallyblock and a cou­ple exper­i­men­tal fea­tures. I now pre­fer it to oth­er mod­ulesthat imple­ment try/​catchsyn­tax like Try::Tinyand TryCatch(even though we use Try::Tinyat work). If all you need is the basic syn­tax above, using Feature::Compat::Trywill get you used to the seman­tics that are com­ingin the next ver­sion of Perl.

Other exception modules (updated)

autodieis nice, and some oth­er mod­ules and frame­worksimple­ment their own excep­tion class­es, but what if you want some help defin­ing your own? After all, an error string can only con­vey so much infor­ma­tion, may be dif­fi­cult to parse, and may need to change as busi­ness require­ments change.

Although CPAN has the pop­u­lar Exception::Classmod­ule, its author Dave Rolsky rec­om­mends that you use Throwableif you’re using Mooseor Moo. If you’re rolling your own objects, use Throwable::Error.

Using Throwablecould­n’t be simpler:

package Foo;

use Moo;
with 'Throwable';

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

... # later...

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

And it comes with Throwable::Error, which you can sub­class to get sev­er­al use­ful 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;
}

(That stack_traceattribute comes cour­tesy of the StackTrace::Autorole com­posed into Throwable::Error. Mooand Mooseusers should sim­ply com­pose it into their class­es to get it.)

Testing exceptions with Test::Exception

Inevitably bugs will creep in to your code, and auto­mat­ed tests are one of the main weapons in a devel­op­er’s arse­nal against them. Use Test::Exceptionwhen writ­ing testsagainst code that emits excep­tions 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();

Note that Test::Exception​’s func­tions don’t mess with $EVAL_ERROR, so you’re free to check its val­ue right after you call it.

Documenting errors and exceptions

If I can leave you with one mes­sage, it’s this: Please doc­u­ment every error and excep­tion your code pro­duces, prefer­ably in a place and lan­guage that the end-​user can under­stand. The DIAGNOSTICS sec­tion of your doc­u­men­ta­tion (you are writ­ing doc­u­men­ta­tion, right, not just code com­ments?)is a great can­di­date. You can mod­el this sec­tion after the perldiagman­u­al page, which goes into great detail about many of the error mes­sages gen­er­at­ed by Perl itself.

(A pre­vi­ous ver­sion of this arti­cle did not note that one should make sure a suc­cess­ful evalreturns true, and incor­rect­ly stat­ed that Class::Exceptionand Throwablewere dep­re­cat­ed due to a bug in the MetaCPAN web site. Thanks to Dan Book for the corrections.)


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Your post must include a link to this post.

How does this work?
Write a response on your own website and include a link to this post. Then submit your response’s URL here. Your response may appear here after moderation. To update or remove it, edit or delete your original post and submit the same URL again. Learn more about Webmentions.