Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for writing exception classes in modern Perl

Tags:

exception

perl

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.

like image 581
Dave Baird Avatar asked Oct 28 '25 14:10

Dave Baird


1 Answers

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" ;
    }
like image 112
Dave Baird Avatar answered Oct 30 '25 03:10

Dave Baird