Home > Net >  Best practice for writing exception classes in modern Perl
Best practice for writing exception classes in modern Perl

Time:11-07

With Exception::Class, I can define exceptions as classes, and they're available everywhere once they've been loaded anywhere. But various places, including the docs for E::C itself, recommend using Throwable nowadays.

Throwable is a role, so I need to build the classes to compose it into. Throwable::Factory helps with that, but I can't figure out how to make these classes available everywhere. It seems T::F builds subroutines that return opaque class names. I feel I'm missing the last piece of the puzzle but haven't been able to find any examples of T::F real-world usage.

CodePudding user response:

One idea could be to collect the exceptions in a separate module and import that into all modules that needs access to the exceptions. Unfortunately, it seems like it is difficult to export these for some reason. I tried the following (MyExceptions.pm):

package MyExceptions;
use Throwable::Factory
  GeneralException => undef,
  RuntimeException => undef,
;
our @EXPORT = qw(GeneralException RuntimeException);
sub import2 {
    no strict 'refs';

    my $caller = caller;
    my $pkg = __PACKAGE__;
    for my $name (@EXPORT) {
        my $imported = $caller . '::' . $name;
        my $coderef = *{$pkg . "::" . $name};
        *{ $imported } = \*{ $coderef };
    }
}

sub import {
    no strict 'refs';

    my $caller = caller;
    my $pkg = __PACKAGE__;
    my @coderefs = (
        ["GeneralException", *MyExceptions::GeneralException],
        ["RuntimeException", *MyExceptions::RuntimeException]
    );
    for my $item (@coderefs) {
        my ($name, $coderef) = @$item;
        my $imported = $caller . '::' . $name;
        *{ $imported } = \*{ $coderef };
    }
}

1;

I was not able to make the import2() exporter sub in the above code work (it does not export the exception but something else (but what?)), so as a workaround I wrote the import() sub which works.

CodePudding user response:

There seem to be 4 things I'm looking for.

  1. Easy syntax for declaring exception types.
  2. Exceptions that implement Throwable.
  3. Extra features in the exception objects, like tags, custom attributes, and simplification of passing attributes to the constructor.
  4. Instantiation of exceptions via classes (globally available) rather than functions (which have to be imported into every module).

Like Exception::Class, Throwable::Factory and Throwable::SugarFactory offer a condensed syntax for declaring exception types, but it turns out I can live without that. Throwable::Factory in fact has everything I want, except the exception functions have to be declared in the same file they're used in. They're kind of throwawayable-throwable exceptions. I don't want that.

Some of the extra features in Throwable::Factory exceptions come from Throwable::Error, which is part of the Throwable distribution. The rest are easy enough to steal. Throwable::Error is in fact a Moo class, and so we have a winner.

I can put all my exception classes in a single file and load it via use at the top of my app. The exception hierarchy inherits from Throwable::Error as a base class. Because these are Moo classes, it's trivial to add custom accessors to particular classes. And I can cut/paste the extra features I like from Throwable::Factory.

package MyApp::Exceptions ;
use strict ;
use warnings ;

use Throwable::Error ;
use Types::Standard qw( Str ) ;
use Moo ;
use namespace::clean ;

use feature qw(signatures) ;
no warnings qw(experimental::signatures) ;

extends 'Throwable::Error' ;

with 'Role::Identifiable::HasTags' ;

has description => ( 
    is => 'ro', 
    isa => Str, 
    required => 1, 
    default => 'Generic exception',
    ) ;
   
# stack_trace() and message() inherited from Throwable::Error 
sub error   ($self) { $self->message  }
sub package ($self) { $self->stack_trace->frame(0)->package  }
sub file    ($self) { $self->stack_trace->frame(0)->filename  }
sub line    ($self) { $self->stack_trace->frame(0)->line  }

# sugar for ::HasTags 
sub has_tags ( $self, @wanted ) {
    $self->has_tag($_) || return 0 for @wanted ;
    return 1 ;
    }

# support shorthand instantiation eg Foo->throw($message, attr => $val);
around BUILDARGS => sub {
    my ( $orig, $class, @args ) = @_ ;
    return  {} unless @args ;
    return $class->$orig(@args) if @args == 1 ;
    unshift @args, 'message' if @args % 2 ;
    return $class->$orig( {@args} ) ;
    } ;

# ----- enduser exception classes -----
package SystemError ;
use Types::Standard qw( Int ) ;
use Moo ;
extends 'MyApp::Exceptions' ;
has code           => ( is => 'ro', isa => Int->where('$_ >= 0'), default => 1 ) ;
has ' description' => ( default => 'A system error' ) ;

package FileError ;
use Types::Standard qw( InstanceOf ) ;
use Moo ;
extends 'SystemError' ;
has ' code'        => ( default => 2 ) ;
has ' description' => ( default => 'A file error' ) ;
has file           => ( is => 'ro', required => 1, isa => InstanceOf['Path::Tiny'] ) ;

1 ;

As long as somewhere, I've said use MyApp::Exceptions;, now everywhere I can say:

use Nice::Try ;

try {
    something() or SystemError->throw("Problem trying to do something", 
        code => 7, 
        tags => [qw(something broke)],
        ) ;
    }

catch ( SystemError $e where { $_->has_tags(qw(something broke)) }) {
    fix_it($e) ;
    }

catch ( SystemError $e where { $_->has_tag('something') }) {
    repair_it($e) ;
    }

catch ( FileError $e ) {
    warn sprintf "Problem doing something() with file %s: %s", 
        $e->file->basename, $e->message ;
    }

catch ( $e ) {
    die "Give up! $e" ;
    }
  • Related