Previous Page
Next Page

13.9. OO Exceptions

Use exception objects whenever failure data needs to be conveyed to a handler.

Since Perl 5.005 it has been possible to pass a single blessed reference to die or croak. Suppose, for example, you've created an exception class[*] named X::TooBig. Then you can create an X::TooBig object and pass it straight to die or croak:

[*] The "Exception Classes" guideline later in this chapter shows how to implement these types of classes.

    croak( X::TooBig->new( {value=>$num, range=>[0,$MAX_ALLOWED_VALUE]} ) )
        if $num > $MAX_ALLOWED_VALUE;

Using objects as an exception has two important advantages: exception objects can be detected by type (using the exception classes' caught( ) methods), and they can ferry complex data structures back to an exception handler, carrying them inside the exception objects. For example:


    # Get the next number...
my $value = eval { get_number( ) };
# If the attempt fails...
if ($EVAL_ERROR) {
# If the candidate was considered too big, go with the maximum allowed...
if ( X::TooBig->caught( ) ) { my @range = $EVAL_ERROR->get_range( ); $value = $range[-1]; }

        # If the candidate was deemed too small, try it anyway...
elsif ( X::TooSmall->caught( ) ) { $value = $EVAL_ERROR->get_value( ); }
# Otherwise, rethrow the exception...
else { croak( $EVAL_ERROR ); } }

Here, the exception coming back from get_number( ) is an object, so you can check it against each exception class to which it might belong:

    if ( X::TooBig->caught(  ) ) {
# [Handle "Too Big" problem]
} elsif ( X::TooSmall->caught( ) ) {
# [Handle "Too Small" problem]

When it's identified, you can then call methods on it:

    my @range = $EVAL_ERROR->get_range(  );

to recover information about the problem.

If that exception had been string-based:

    croak( "Numeric value $num too big (must be $MAX_ALLOWED_VALUE or less)" )
        if $num > $MAX_ALLOWED_VALUE;

then you'd need to use regular expressions to recognize the exception, identify the necessary information in the string, and extract it:

    # Get the next number...
    my $value = eval { get_number(  ) };

    # If the attempt fails...
    if (defined $EVAL_ERROR) {
        # If the candidate was considered too big, go with the maximum allowed...
        if ($EVAL_ERROR =~ m{ \A Numeric [ ] value [ ] \S+ [ ] too [ ] big }xms) {
            ($value) = $EVAL_ERROR =~ m{ must [ ] be [ ] (\S+) [ ] or [ ] less }xms;
        # If the candidate was deemed too small, try it anyway...
        elsif ($EVAL_ERROR =~ m{ \A Numeric [ ] value [ ] (\S+) [ ] too [ ] small }xms) {
            $value = $1;
        # Otherwise, rethrow the exception...
        else {
            croak( $EVAL_ERROR );

This is clearly an inferior solution to object-based exceptions. Most obviously, it produces code that's more cumbersome and less readable. More importantly, it requires the code to stringify important data that might help with recovery, then re-extract that data to actually perform the recovery. That, in turn, restricts exceptions to passing data that can be serialized and deserialized.

In contrast, exception objects can channel any type of Perl data type back to an exception handler. For example:

    croak( X::EOF->new( {handle=>$fh} ) )
        if $fh->eof(  );

Because the resulting exception object carries the actual filehandle with it, a handler in some outer scope has the possibility of rewinding that handle and trying again:

    sub try_next_line {
# Give get_next_line(  ) two chances...
for my $already_retried (0..1) {
# Return immediately on success, but catch any failure...
eval { return get_next_line( ) };
# Rethrow the caught exception if it isn't an EOF problem...
croak( $EVAL_ERROR ) if !X::EOF->caught( );
# Also rethrow the caught exception
            # if we've already tried rewinding the filehandle...
croak( $EVAL_ERROR ) if $already_retried;
# Otherwise, try rewinding the filehandle...
seek $EVAL_ERROR->handle( ), 0, 0; } }

In this example, try_next_line( ) gives get_next_line( ) two tries to return some data, by putting the call to get_next_line( ) in a for loop that iterates twice. It first attempts to call get_next_line( ) and return the result. If that attempt succeeds, then the call to TRy_next_line( ) is done[*]. But if get_next_line( ) throws an exception, then the return will not be executed, the eval will catch the exception, and the rest of the for loop will proceed.

[*] And, more importantly, no error-handling overheads were incurred. This is yet another advantage of using exceptions: they allow you to optimize for successful behaviour.

The second statement in the loop will check whether the failure was due to an X::EOF exception (which the remainder of the loop may be able to handle) or if some unknown exception was thrown, in which case that exception will simply be rethrown (croak $EVAL_ERROR).

The loop then checks whether this is the second time an exception has been encountered in this call to try_next_line( ) and, if the subroutine has already retried get_next_line( ), it immediately gives up and repropagates the exception.

Finally, the loop grabs the offending filehandle that was passed back in the exception object ($EVAL_ERROR->get_handle( )) and seeks it back to its start-of-file position. The loop then reiterates, to give get_next_line( ) its second, and final, chance.

Of course, this is possible only because the exception was an object, and able to carry the filehandle in question back to try_next_line( ). Using a string-based exception:

    croak( "Filehandle $fh at EOF" )
        if $fh->eof(  );

would have made that impossible, since the exception that try_next_line( ) received back would have been something like:

    Filehandle GLOB(0x800368) at EOF at line 420

in which case try_next_line( ) would have no way to access the original filehandle, and hence no way to "fix" it and try again.

    Previous Page
    Next Page