Day the Seventh: Error Handling

Error Handling

Top Close Open

We looked yesterday at the error() method provided by Badger::Base for reporting errors.

Here's a simple module showing the method in use.

package HAL;
use base 'Badger::Base';

sub open_pod_bay_doors {
    my $self = shift;
    $self->error("I'm sorry Dave, I'm afraid I can't do that");
}

1;

If we create a HAL object and call the open_pod_bay_doors() method, we get the error thrown as an exception using Perl's inbuilt die() function.

use HAL;
my $hal = HAL->new;
$hal->open_pod_bay_doors;

If we don't do anything else to catch the error then the Perl program will exit and the error will be reported.

hal error - I'm sorry Dave, I'm afraid I can't do that

The error is thrown as a Badger::Exception object. This stores the error message ("I'm sorry Dave, I'm afraid I can't do that") along with an error type derived (by default) from the name of the class in which the error was raised. In this case the module is HAL which results in a lower case exception type of hal. Any occurrences of :: in your module name are replaced with dots (e.g. Your::Module has a default error type of your.module), although that doesn't apply in this simple example.

The Badger::Exception object has a text() method which is automatically called when the object is stringified (e.g. when printed, appended onto another string, etc). It's that method that generates the summary shown above.

Catching Errors

Top Close Open

You can catch an error by enclosing code in an eval block (this is Perl's equivalent of try in other languages). If an error is thrown then it will be defined in the magic $@ variable.

eval {
    $hal->open_bay_doors;
};
if ($@) {
    print "Caught error: $@";
}

This will now print the following message:

Caught error: hal error - I'm sorry Dave, I'm afraid I can't do that.

The $@ variable contains the Badger::Exception object. Instead of just printing it out, we can inspect the type and/or message separately.

if ($@) {
    print "Error type: ", $@->type, "\n";
    print "Error info: ", $@->info, "";
}

The try() Method

Top Close Open

The Badger::Base module provides the try() method as a convenient short-cut to using eval.

Instead of writing this:

eval {
    $hal->open_pod_bay_doors;
}
if ($@) {
    print "Error caught: $@";
}

You can write this:

$hal->try('open_pod_bay_doors')
    || print "Error caught: ", $hal->error;

The try() method calls the method named by the first argument, wrapping it up in an eval block to catch an errors throw. If an error is caught then the method returns undef. You can then call the error() method without any arguments to return the Badger::Exception object representing the error. Or you can call the reason() method which returns the same thing.

$hal->try('open_pod_bay_doors')
    || print $hal->reason;

You can specify any additional arguments you want passed to the method following the method name:

$hal->try( open_pod_bay_doors => 'please' )
    || print $hal->reason;

When is an Error Not an Error?

Top Close Open

Errors should always be thrown as exceptions. Don't be tempted to return undef from a method to indicate failure. It's a bad practice that leads to brittle code (because people invariable forget to check the return value) and overly verbose code that is littered with error checking (in the event that they don't).

However, that doesn't mean that there aren't cases where returning undef from a method is the right thing to do. For example, suppose you have a method which fetches a user record from a database. It's quite likely that the method may be asked to fetch a user that doesn't exist. For example, if the code sits behind a login box on a web application then there will inevitably be people who mistype their username, or maliciously try to break in by guessing username and password combinations.

That kind of situation should usually not be considered an error. The method should return the record if found, or decline by returning undef if not. The decline() method can be used to this effect. It stores an error message internally then returns undef

sub fetch_user_by_id {
    my $self = shift;
    my $uid  = shift
        || return $self->error("No user ID specified");

    my $user = ...do something...

    return $user
        || $self->decline("No such user: $uid")
}

The calling code is then expected to check for the undefined value. It can call the reason() (or error()) methods to fetch the decline message generated. Something like this, perhaps:

sub my_web_app_handler {
    my $self = shift;
    my $uid  = $self->param('uid');
    my $user = $self->fetch_user( id => $uid )
        || return $self->error_page(
                "You're a very naughty boy: ",
                $self->reason
           ); 

    # more code here
}

Note that we only need to worry about the declined value in the calling code. If the fetch_user() method really does encounter an error, say if the database isn't connected, or if the caller forgot to specify an id parameter, then it will throw an exception and break out of the normal program flow. You can then catch all uncaught exceptions at a higher level of your program and deal with them as appropriate.

For example, your top-level web application dispatcher might do this:

sub my_main_web_app_dispatcher {
    my $self = shift;

    $self->try('my_web_app_handler')
        || return $self->error_page(
                "An internal error occurred.",
                "We're really sorry",
                $self->reason
            );
}

Which is little more than syntactic sugar for this:

sub my_main_web_app_dispatcher {
    my $self = shift;

    eval {
        $self->my_web_app_handler;
    };
    if ($@) {
        return $self->error_page(
            "An internal error occurred.",
            "We're really sorry",
            $@
        );
    }
}

This approach ensures that all errors get caught and reported.

The general rule here is that a method that encounters an error should never return. If a method does return then we assume that it successfully performed whatever task it was supposed to perform. It might be the case that the method returns undef, as in our earlier example to indicate that it couldn't find the requested record in the database. However, the fact that it returned at all indicates that it successfully looked for the record and can confirm that it doesn't exist.

Of course there may be times when a missing database record really does constitute an error. For example, a method that sends out an invoice for an order cannot succeed if the order does not exist. In that case, the method is responsibly for reporting the missing record as an error.

sub generate_order_invoice {
    my $self  = shift;
    my $oid   = shift 
        || return $self->error("No order ID specified");

    # get_order_record() may decline with undef, but that's not
    # good enough for us... we *must* have an order record to proceed
    my $order = $self->get_order_record($oid)
        || return $self->error("Order $oid not found!");

    # ...count beans, generate invoice, ???, profit...
}
Fork Me on Github