Quantcast
Viewing latest article 5
Browse Latest Browse All 9

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.


Viewing latest article 5
Browse Latest Browse All 9

Trending Articles