Perl Best Practices
13.13. Exception Classes
Build exception classes automatically. As the preceding guidelines illustrate, using objects as exceptions can significantly improve the robustness and future maintainability of your error-handling code. There is, however, a downside: you have to build the exception classes to instantiate those exceptions. And those exception classes need to be reasonably sophisticated in order to work correctly. For example, they need to provide for throwing, rethrowing, and identifying exceptions; they need to provide the appropriate internal storage for preserving the error information and context; and they need some kind of stringification overloading (see Chapter 16) to ensure that they still produce sensible error messages in string contexts: for example, when they're printed out as they terminate a program. A minimal hash-based implementation of the X::EOF class used in the previous guidelines of this chapter is shown in Example 13-2. Example 13-2. Minimal X::EOF exception class
# Define the class representing end-of-file exceptions... package X::EOF; use Carp; # Make X::EOF objects stringify to the same message used previously... use overload ( q{""} => sub { my ($self) = @_; return "Filehandle $self->{handle} at EOF $self->{caller_location}"; }, fallback => 1, ); # Create a X::EOF exception... sub new { my ($class, $args_ref) = @_; # Allocate memory for the object and initialize it... my %self = %{$args_ref}; # If no filehandle is passed, indicate that it's unknown... if (! exists $self{handle}) { $self{handle} = '(unknown)'; } # Ask Carp::shortmess() where croak( ) would report the error occurring... if (!exists $self{caller_location}) { $self{caller_location} = Carp::shortmess( ); } # Add it to the class and send it on its way... return bless \%self, $class; } # Give access to the handle that was passed into the constructor... sub get_handle { my ($self) = @_; return $self->{handle}; } # Test whether the currently propagating exception is of this type... sub caught { my ($this_class) = @_; use Scalar::Util qw( blessed ); return if !blessed $EVAL_ERROR; return $EVAL_ERROR->isa($this_class); } Of course, the processes of creating exception objects, overloading their stringification behaviours, and helping them work out where they were created, are essentially identical for all exception classes, so those methods could be factored out into a common base class, as shown in Example 13-3. Example 13-3. Refactoring the X::EOF exception class
# Abstract the common behaviours of all exception classes... package X::Base; # Make exception objects stringify to an appropriate string... use overload ( q{""} => sub { my ($self) = @_; return "$self->{message} $self->{caller_location}"; }, fallback => 1, ); # Create the base object underlying any exception... sub new { my ($class, $args_ref) = @_; # Allocate memory for the object and initialize it... my %self = %{$args_ref}; # Make sure it has an error message, building one if necessary... if (! exists $self{message}) { $self{message} = "$class exception thrown"; } # Ask Carp::shortmess()where croak( ) would report the error occurring # (but make sure Carp ignores whatever derived class called this # constructor, by temporarily marking that class as being "internal" # and hence invisible to Carp)... local $Carp::Internal{caller( )} = 1; if (!exists $self{caller_location}) { $self{caller_location} = Carp::shortmess( ); } # Add it to the class and send it on its way... return bless \%self, $class; } # Test whether the currently propagating exception is of this type... sub caught { my ($this_class) = @_; use Scalar::Util qw( blessed ); return if !blessed $EVAL_ERROR; return $EVAL_ERROR->isa($this_class); } # Define the X::EOF class, inheriting useful behaviours from X::Base... package X::EOF; use base qw( X::Base ); # Create a X::EOF exception... sub new { my ($class, $args_ref) = @_; if (! exists $args_ref->{handle}) { $args_ref->{handle} = '(unknown)'; } return $class->SUPER::new({ handle => $args_ref->{handle }, message => "Filehandle $args_ref->{handle} at EOF", }); } # Give access to the handle that was passed into the constructor... sub get_handle { my ($self) = @_; return $self->{handle}; } As you can see, even with some of the effort being amortized into a common base class, there's still a considerable amount of tedious work required to create any exception class. A much cleaner solution is to use the Exception::Class CPAN module instead. This module provides a powerful predefined base class for exceptions. It also offers an easy way to create new exception classes that are derived from that base class, allowing you to quickly add in any extra attributes and methods that those new classes might require. For example, using Exception::Class, you could build the complete X::EOF class in under 10 lines of code:
# Define the X::EOF class, inheriting useful behaviours # from Exception::Class::Base... use Exception::Class ( X::EOF => { # Specify that X::EOF objects have a 'handle' attribute # and a corresponding handle( ) method... fields => [ 'handle' ], }, ); # Redefine the message to which an X::EOF object stringifies... sub X::EOF::full_message { my ($self) = @_; return 'Filehandle ' . $self->handle( ) . ' at EOF'; } Throwing the exception using this version of X::EOF would remain essentially the same, with only the slight syntactic difference that Exception::Class constructors pass their arguments as raw pairs, rather than in hashes. That is, you would now write: croak( X::EOF->new( handle => $fh ) );
Better still, classes built with Exception::Class provide an even easier way to create and throw their exception: X::EOF->throw( handle => $fh );
Exception::Class has many other features that help with handling fatal errors. Exceptions built with the module can produce full call-stack traces; can rethrow themselves in a very simple and clean way; can report user, group, and process IDs; and can create and export "alias subroutines", which can further simplify exception throwing. The module is highly recommended. |