Using exception classes in Perl

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 ::FailedJobs, 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.

This entry was posted in Perl by dakkar. Bookmark the permalink.

About dakkar

Gianni is a Perl Architect at NAP. His code from previous lives runs in universities administration software, inside ask.com news system, and even in Antarctica. He's currently busy writing libraries to make "the right thing" be "the easy thing".

2 thoughts on “Using exception classes in Perl

  1. A good programs contains a large number of exceptions: conditions which disrupt the usual flow. Not rarely about 1 per 4 lines of logic. Either you combine many errors into one exception (less clear), or you have a lot of exceptions to program (and test). Besides, you often get into the difficulty of maintaining an exception hierarchy. Maintaining hierarchies with many components is quite hard.

    An alternative is to use tags in stead of classes. For instance, in Log::Report, an (user-)error exception is thrown like this: (includes delayed optional translation)

    @files >= 3
    or error __x”three files are needed, I got {count}”, count => scalar @files,
    _class => ‘user,show_help, unrecoverable’;

    IMHO, the lighter the syntax for exceptions is, the better the error reporting gets. During YAPC::EU, Moritz spoke about the new Perl6 exception system, and that a number old errors still had to be translated to the new system. When the syntax is light, it is a no-brainer.

    I do not like classes in a program which are only rarely used, for a kind of singleton objects. It would also be possible to create a set of exception classes (for instance in the “use warnings” category) as base, and then use tags on top of those.

Leave a Reply