Exception handling in Perl is, like OO support, very basic: die
to
throw an exception, eval
to catch it into the global $@
variable. What an exception is, what to do when you’ve caught it, and
other such niceties are just not part of the language.
Let’s start from the “throwing” end…
The traditional way
The simple, traditional, and excessively widespread way of signalling
failure or exceptional events (apart from returning special values
like undef
) is to die
with a human-readable error message. This is
sort-of fine when you don’t actually plan on catching the exception:
it gets printed to stderr, the program terminates, and that’s it.
But if you want to perform some kind of recovery when an exception is
thrown, and if what you need to do depends on exactly why it was thrown,
you have problems. Of course, you can match the string against some
regexp and decide based on that, but then you have tied the semantics
of your program to the wording of the error message, which is a user
interface detail. Things get even more complicated if you need to
extract information from the error message, such as which primary key
failed to update in a DBI
exception.
A better idea: exception objects
We would like a way to throw a data structure with a recognisable
name. This is exactly what an object reference is, and die
can throw any kind of scalar (although throwing a globref or a filehandle is probably a bad idea).
In addition, by using exception objects, you can group your exception
classes in a hierarchy, or even a mesh of ->isa
or ->DOES
.
There are several CPAN distributions of exception classes. We have
decided to use Throwable
as a base, and to add some features “stolen” from
Throwable-X
. The result
is NAP::Exception
.
NAP::Exception
NAP::Exception
consumes Throwable
and StackTrace::Auto
(yes, we
almost always want a full stack trace), declares a required message
attribute, and provides an as_string
method (also accessible via
overloaded stringification). as_string
uses a formatter based on
String::Errf
that will
replace every %{foo}s
in the message
with the value obtained by
calling the foo
method on the exception object.
Our NAP::policy
module has a special import option to set up the
calling package as a NAP::Exception
subclass.
The idea is that you write:
package MyApp::Exception::FailedJob {
use NAP::policy 'exception';
has job_id => ( is => 'ro', required => 1 );
has '+message' => ( default => 'Job %{job_id}d failed at %{stack_trace}s' );
}
package MyApp::Exception::FailedJob::DBProblem {
use NAP::policy 'exception';
extends 'MyApp::Exception::FailedJob'; has db_exception => ( is => 'ro', required => 1 );
has '+message' => ( default => 'Job %{job_id}d failed because of db problems (%{db_exception}s) at %{stack_trace}s' );
}
And so on. Then you can either catch all ::FailedJob
s, handle the
DB-caused ones differently, or just die and let the message get
printed to the user.
If you need a light-weight exception, because you know that you’ll
throw it just to catch it a few frames out, you can consume the
NAP::Exception::Role::NoStackTrace
role to avoid creating a stack
trace you wouldn’t use.
simple_exception
After having written 3-4 exception classes, you start to understand
why the
Exception::Class
module allows you to just declare the exceptions with very little
typing. So, we wrote something similar, a function called
simple_exception
that does most of the scaffolding for you. The
above classes could then be written this way:
package MyApp::Exception {
use NAP::policy 'simple_exception';
simple_exception(
'FailedJob',
'Job %{job_id}d failed at %{stack_trace}s',
{ attrs => [ 'job_id' ] }
);
simple_exception('FailedJob::DBProblem',
'Job %{job_id}d failed because of db problems (%{db_exception}s) at %{stack_trace}s',
{ extends => 'FailedJob',
attrs => [ 'db_exception' ] }
);
}
This seems to be the bare minimum you need to write: class name, message pattern, attribute names, and maybe a superclass. Class names are taken to be “under” the calling package namespace, but you can still use fully-qualified superclasses if you need to.
Catching exceptions
Of course, if all you were going to do with exceptions is to throw them, you might as well have kept throwing strings. The power of exception objects is evident when you start catching them.
But first, a small diversion on the various ways of catching exceptions.
Plain eval
eval { code_that_may_throw() };
if ($@) {
if (blessed($@) and $@->isa('MyApp::Exception::FailedJob')) {
log_failed_job($@->job_id);
}
else {
log_errors("Something bad happened: $@");
}
}
This is the basic way of catching. It works. Mostly. In addition to
being, frankly, ugly and verbose, there are a few issues with it: $@
gets overwritten by each eval
(which may well happen in a DESTROY
method, called while unwinding the stack), in some weird cases $@
might not actually be true, and a few others.
Try::Tiny
Try::Tiny
fixes all the problems mentioned above, apart from some of
the ugliness:
use Try::Tiny;
try { code_that_may_throw() }
catch {
if (blessed($_) and $_->isa('MyApp::Exception::FailedJob')) {
log_failed_job($_->job_id);
}
else {
log_errors("Something bad happened: $_");
}
};
You could make things a bit more legible with when
and default
,
but you still have to use blessed
to avoid calling ->isa
on a
non-object just in case something threw a string (and it will happen).
TryCatch
TryCatch
uses the scary power of Devel::Declare
to provide a very
sugary syntax (in addition to avoiding all the standard eval
problems):
use TryCatch;
try { code_that_may_throw() }
catch (MyApp::Exception::FailedJob $e) {
log_failed_job($e->job_id);
}
catch ($e) {
log_errors("Something bad happened: $e");
}
It’s got its fair share of problems, though: you cannot use try
as
an expression (you can with eval
and Try::Tiny
), return
sometimes behaves weirdly inside a catch
block, and in a few of our
applications it segfaults during global destruction (which does not
affect any actual work, but makes Ops people justifiably jumpy).
At the moment, NAP::policy
injects TryCatch
in our packages, but
we’re working out how to move to Try::Tiny
, which hides fewer
surprises.
Example of use: let DBI throw our exceptions
As an interesting example of using structured exceptions, consider the following scenario:
I have an application that can store and modify documents into several different storage engines. Some use DBI and transactions, some use MongoDB and optimistic concurrency control. I want to be able to process documents regardless of the engine in use.
The basic structure of the application is:
my $iter = $storage->all_docs();
while (my ($id,$doc) = $iter->next()) {
process($doc);
$storage->update($id,$doc);
}
$storage->commit();
The MongoDB storage will not do anything on ->commit()
, but the
DBI
may throw an exception if the transaction failed. On the other
hand, the DBI
storage will never have problems on ->update
because
we lock the rows when getting them, but MongoDB
may detect a
concurrent modification and throw an exception.
So we rewrite our loop:
my $done = 0;
while (!$done) {
try {
my $iter = $storage->all_docs();
while (my ($id,$doc) = $iter->next()) {
process($doc);
try {
$storage->update($id,$doc);
} catch (MyApp::Exception::ConcurrentModification $e) {
$doc = $storage->get_doc($id);
redo;
}
}
$storage->commit();
$done=1;
} catch (MyApp::Exception::TransactionFailed $e) {
$done=0;
}
}
A bit more complex, but still pretty clear: keep trying until you can commit, and re-process any document that failed to update.
Getting the MongoDB storage to throw the ConcurrentModification
exception is quite easy, since you detect that by noting that an
update
affected 0 documents; we can throw it manually.
Getting DBI
to throw our own kind of exceptions is… different. You
probably already know that you can get DBI
to throw exceptions by
setting the RaiseError
attribute on the db handle, but reading the
documentation we can see that you can also set the HandleError
attribute, to a coderef that will get passed the error message. We can
then do something like this:
sub _build_dbh {
my ($self) = @_;
my $dbh = DBI->connect(
$self->dsn,
$self->db_username,
$self->db_password,
{
PrintError => 0,
RaiseError => 0,
HandleError => &_dbi_error_handler,
}
);
return $dbh;
}
sub _dbi_error_handler {
my ($message, $dbh, $ret) = @_;
if ($message =~ m{\bduplicate key value violates unique constraint\b}) {
MyApp::Exception::ConcurrentUpdate->throw();
}
elsif ($message =~ m{\bcurrent transaction is aborted\b}) {
MyApp::Exception::TransactionFailed->throw();
}
else {
MyApp::Exception::DBI->throw({
message => "$message at %{stack_trace}s",
});
}
}
And voila, DBI
will throw our exceptions, and the rest of the
application does not have to deal with trying to parse error strings
or wondering which storage engine it’s using.
For a more comprehensive solution, see DBIx::Error
.