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";
(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
}
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";
}
};
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};
}
}
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'} );
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;
}
(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();
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.)
Top comments (2)
Great article as usual, thank you for having switched on the light a bit more on exceptions for me 😃
Thanks! I learned about autodie's exception objects while writing this, so it helps me too!