2010-08-04

Extending Exception::Class in Moose

I recently had the joyous experience of porting some code to use proper Object Oriented Exceptions, and found a few niggles in my experience.

Exception::Class is a great module, and in terms of an Exception base class does lots of the things I want an exception module to do.

However, it has one and only one really big problem from my perspective, and that is, by default, its extensibility is a bit limited.

It appears to be highly targeted for its in-line declarations at import(), as follows:

 use Exception::Class (
      'MyException',

      'AnotherException' => { isa => 'MyException' },

      'YetAnotherException' => {
          isa         => 'AnotherException',
          description => 'These exceptions are related to IPC'
      },

      'ExceptionWithFields' => {
          isa    => 'YetAnotherException',
          fields => [ 'grandiosity', 'quixotic' ],
          alias  => 'throw_fields',
      },
  );

This is handy, For the simple case. But it doesn't do you a whole bunch of favours. Adding custom methods is a bane, and there's no field validation/processing support.

The best alternative to getting custom methods is Exception::Class::Nested which lets you do this:

        use Exception::Class::Nested (
                'MyException' => {
                        description => 'This is mine!',

                        'YetAnotherException' => {
                                description => 'These exceptions are related to IPC',

                                'ExceptionWithFields' => {
                                        fields => [ 'grandiosity', 'quixotic' ],
                                        alias => 'throw_fields',
                                        full_message => sub {
                                                my $self = shift;
                                                my $msg = $self->message;
                                                $msg .= " and grandiosity was " . $self->grandiosity;
                                                return $msg;
                                        }
                                }
                        }
                },
        );

This is loads more practical, merely by eliminating the isa => stuff and adding of custom methods, but it still lacks many things in extensibility. No Type checking, no parameter processing, and worst of all, no apparently logical path to avoid clobbering parent methods ( I'm entirely assuming the ->SUPER:: stuff works, but I dislike that peskyness with a passion ). And last but not least, that module won't even install or pass its own tests.

So, you find yourself to this sort of thing:

use strict;
use warnings;
package ExceptionWithFields;
use base 'YetAnotherException';
# Every time I have to do this, I forget how to do it
# which is especially annoying as its not documented anywhere
# and Exception::Class bolts it on to its generated exceptions during ->import()
# so the method is nowhere to be found in Exception::Class::Base 's code 
# or its inheritance hierarchy.
# the inner guts of it are hidden away in Exception::Class::_make_subclass
sub Fields {
     # return an array of field names or they won't get populated.
     return ('grandiosity', 'quixotic');
}

# yes, you have to write your own accessors
# Exception::Class->import() generates these accessors manually.

sub grandiosity { 
    my ( $self ) = shift;
    return $self->{grandiosity};
}

sub quixotic {
    my ( $self ) = shift;
    return $self->{quixotic};
}

sub full_message {
    my $self = shift;
    my $msg = $self->message;
    $msg .= " and grandiosity was " . $self->grandiosity;
    return $msg;
}

1;

YUCK!. . That's an awfully lot of nasty boilerplate :(.

This is only a simple example, so you can see how it'd get more complicated with more advanced things, I don't even want to contemplate how to handle parameter coercion/processing.

So, lets Moose this thing up!

I'm addicted to this Moose thing.

Moose probably makes Exception classes overweight, but considering how short lived they are, in many cases it doesn't really matter.

Unfortunately for us, Exception::Class uses some other weird thing which makes bolting stuff on to it a bit harder.

But fortunately, there is MooseX::NonMoose which makes this mostly painless.

use strict;
use warnings;
package MyException;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
extends qw(Exception::Class::Base);
# This method is needed to delete things which are supposed to be handled by Moose 
# so they don't get passed to the parent constructor, because excess args cause it to fail -_-
sub FOREIGNBUILDARGS {
  my ( $class, %args ) = @_;
  for ( $class->meta->get_attribute_list ) {
    delete $args{$_};
  }
  return %args;
}
# Handy addition for giving back traces to user-land.
around show_trace => sub {
  my ( $orig, $class, @rest ) = @_;
  return 1 if exists $ENV{MYEXCEPTION_STACKTRACE} and $ENV{MYEXCEPTION_STACKTRACE};
  return $class->$orig(@rest);
};
__PACKAGE__->meta->make_immutable;

Yay. Suddenly we have something Moose friendly that JustWorks as we want it to. And we've already added functionality by making all our children's stack-traces forced on by an ENV option, but otherwise behave as usual.

Now for the derivative classes

use strict;
use warnings;
package YetAnotherException;
use Moose;
use namespace::autoclean;
extends 'MyException'; 
__PACKAGE__->meta->make_immutable();
1;
use strict;
use warnings;
package ExceptionWithFields;
use Moose;
extends 'YetAnotherException'; 
use namespace::autoclean;

has 'grandiosity' => ( isa => 'Str', is => 'ro', required => 1 );
has 'quixotic' => ( isa => 'Str', is => 'ro' , required => 1 );

# Now with inheritable message code =)
around full_message => sub {
    my ( $orig, $self , @args ) = @_;
    my $msg = $self->$orig( @args );
    $msg .= " and grandiosity was " . $self->grandiosity;
    return $msg;
};

# Stick some lines *after* the stacktrace =D 
around as_string => sub { 
    my ( $orig, $self , @args ) = @_;
    my $msg = $self->$orig( @args ); 
    $msg .= "\n\n Please refer to the ExceptionWithFields Manual for more information"; 
    return $msg;
}
__PACKAGE__->meta->make_immutable();

WAAAY More fun. Waaay Less headaches. Moose++

#!/usr/bin/perl
use ExceptionWithFields;

ExceptionWithFields->throw( 
    message     => "This is a test",
    grandiosity => "This is grand!",
    quixotic    => "Very!",
);

5 comments:

  1. Custom methods are easy to add - just specify the full name - such as sub MyException::growl { ...; } - when you're defining them. I do it that way in Perl::Dist::WiX's extensions classes.

    The other stuff, yeah, keep going. Looks pretty good!

    ReplyDelete
  2. You might also want to look at Throwable and Throwable::Error, which are reactions to Exception::Class.

    ReplyDelete
  3. Thanks fREW. I'll definitely have to try using that at some stage, it looks sweet =)

    ReplyDelete
  4. I realize that this is a year old, but I am having trouble trying to add an errno field to my exception classes and am considering using moose but don't wan't to add another dependency just to add an errno field to my exceptions.

    Can you explain to me why your ExceptionWithFields package has this for line 4:
    use base 'YetAnotherException';

    Should that be:
    use base 'Exception::Class::Base'

    Well I tried that and I can't seem to properly extend exception base class. Any thoughts?

    ReplyDelete
  5. Indeed Moose does work well to extend Exception::Class and create useful ones with custom methods, but how can something like this be accomplished with the Moose approach:

    use My::Exception::Class; # load our custom Moose exception
    use Exception::Class
    (
    'My::Exception::Shell' =>
    { isa => 'My::Exception::Class' },

    'My::Exception::Initialization' =>
    {
    isa => 'My::Exception::Shell',
    description => 'The shell was never initialized',
    alias => 'init_exception',
    errno => 1,
    },
    ...

    I am having immense trouble trying to get the errno to come through to the Moose subclass. I can't think of where to look.

    ReplyDelete