Sweeter Perl exception classes
Tags: perl, oop, exceptions, moo
Published: 2021-12-07 20:51:07

---
title: Sweeter Perl exception classes
published: true
date: 2021-12-07 15:00:00 UTC
tags: Perl,OOP,exceptions,Moo
canonical_url: https://phoenixtrap.com/2021/12/07/sweeter-perl-exception-classes/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ef2nmoovaa38x4p5cy66.jpeg
---

I [mentioned](https://phoenixtrap.com/2021/11/23/video-a-year-of-being-wrong-on-the-internet/) at the [Ephemeral Miniconf](https://thibaultduponchelle.github.io/the-ephemeral-miniconf/) last month that as soon as I write about one Perl module (or five), someone inevitably brings up another (or seven) I’ve missed. And of course, it happened again [last week](https://phoenixtrap.com/2021/11/30/vicious-test-mockery-of-a-perl-modulino/): no sooner had I written in passing that I was using [Exception::Class](https://metacpan.org/pod/Exception::Class) than the denizens of the [Libera Chat IRC #perl channel](https://web.libera.chat/#perl) insisted I should use [Throwable](https://metacpan.org/pod/Throwable) instead for defining my exceptions. (I’ve already blogged about various ways of [catching exceptions](https://phoenixtrap.com/2021/02/22/exceptional-perl-failure-is-an-option/).)

Why Throwable? Aside from Exception::Class’s author [recommending it](https://metacpan.org/pod/Exception::Class#DESCRIPTION) over his own work due to a [“nicer, more modern interface,”](https://metacpan.org/release/DROLSKY/Exception-Class-1.45/source/Changes#L57) Throwable is a [Moo role](https://metacpan.org/pod/Moo::Role), so it’s composable into classes along with other [roles](https://perldoc.perl.org/perlootut#Roles) instead of mucking about with multiple inheritance. This means that if your exceptions need to do something reusable in your application like [logging](https://metacpan.org/pod/Task::Kensho::Logging), you can also consume a role that does that and not have so much duplicate code. (No, I’m not going to pick a favorite logging module; I’ll probably get that wrong too.)

However, since Throwable is a role instead of a class, I would have to define several additional [`package`](https://perldoc.perl.org/functions/package)s in my tiny [modulino](https://perlmaven.com/modulino-both-script-and-module) script from last week, one for each exception class I want. The beauty of Exception::Class is its simple declarative nature: just [`use`](https://perldoc.perl.org/functions/use) it and [pass a list](https://metacpan.org/pod/Exception::Class#DECLARING-EXCEPTION-CLASSES) of desired class names along with options for attributes and whatnot. What’s needed for simple use cases like mine is a declarative syntax for defining several exception classes without the noise of multiple packages.

Enter [Throwable::SugarFactory](https://metacpan.org/pod/Throwable::SugarFactory), a module that enables you to do just that by adding an `exception` function for declaring exception classes. (There’s also the similarly-named [Throwable::Factory](https://metacpan.org/pod/Throwable::Factory); see the above discussion about never being able to cover everybody’s favorites.) The `exception` function [takes three arguments](https://metacpan.org/pod/Throwable::SugarFactory#DECLARATION): the name of the desired exception class as a string, a description, and an optional list of instructions Moo uses to build the class. It might look something like this:

```perl
package Local::My::Exceptions;
use Throwable::SugarFactory;

exception GenericError => 'something bad happened';
exception DetailedError => 'something specific happened' =>
  ( has => [message => ( is => 'ro' )] );

1;
```

Throwable::SugarFactory takes care of creating constructor functions in Perl-style `snake_case` as well as functions for detecting what kind of exception is being caught, so you can use your new exception library like this:

```perl
#!/usr/bin/env perl

use experimental qw(isa);
use Feature::Compat::Try;
use JSON::MaybeXS;
use Local::My::Exceptions;

try {
    die generic_error();
}
catch ($e) {
    warn 'whoops!';
}

try {
    die detailed_error( message => 'you got me' );
}
catch ($e) {
    die encode_json( $e->to_hash )
      if $e isa DetailedError and defined $e->message;
    $e->throw if $e->does('Throwable');
    die $e;
}
```

The above also demonstrates a couple of other Throwable::SugarFactory features. First, you get a `to_hash` method that returns a hash reference of all exception data, suitable for serializing to JSON. Second, you [get all of Throwable’s methods](https://metacpan.org/pod/Throwable::SugarFactory#Throwable), including `throw` for re-throwing exceptions.

So where does this leave last week’s [FOAAS.com](https://foaas.com/) modulino client demonstration of [object mocking](https://en.wikipedia.org/wiki/Mock_object) tests? With a little bit of rewriting to define and then use our sweeter exception library, it looks like this. You can [review](https://phoenixtrap.com/2021/11/30/vicious-test-mockery-of-a-perl-modulino/) for a description of the rest of its workings.

```perl
#!/usr/bin/env perl

package Local::CallFOAAS::Exceptions;
use Throwable::SugarFactory;

BEGIN {
    exception NoMethodError =>
      'no matching WebService::FOAAS method' =>
      ( has => [ method => ( is => 'ro' ) ] );
    exception ServiceError =>
      'error from WebService::FOAAS' =>
      ( has => [ message => ( is => 'ro' ) ] );
}

package Local::CallFOAAS;  # this is a modulino
use Test2::V0;             # enables strict, warnings, utf8

# declare all the new stuff we're using
use feature qw(say state);
use experimental qw(isa postderef signatures);
use Feature::Compat::Try;
use Syntax::Construct qw(non-destructive-substitution);

use WebService::FOAAS ();
use Package::Stash;
BEGIN { Local::CallFOAAS::Exceptions->import() }

my $foaas = Package::Stash->new('WebService::FOAAS');

my $run_as =
    !!$ENV{CPANTEST}       ? 'test'
  : !defined scalar caller ? 'run'
  :                          undef;
__PACKAGE__->$run_as(@ARGV) if defined $run_as;

sub run ( $class, @args ) {
    try { say $class->call_method(@args) }
    catch ($e) {
        die 'No method ', $e->method, "\n"
          if $e isa NoMethodError;
        die 'Service error: ', $e->message, "\n"
          if $e isa ServiceError;
        die "$e\n";
    }
    return;
}

# Utilities

sub methods ($) {
    state @methods = sort map s/^foaas_(.+)/$1/r,
      grep /^foaas_/, $foaas->list_all_symbols('CODE');
    return @methods;
}

sub call_method ( $class, $method = '', @args ) {
    state %methods = map { $_ => 1 } $class->methods();
    die no_method_error( method => $method )
      unless $methods{$method};
    return do {
        try { $foaas->get_symbol("&$method")->(@args) }
        catch ($e) { die service_error( message => $e ) }
    };
}

# Testing

sub test ( $class, @ ) {
    state $stash = Package::Stash->new($class);
    state @tests = sort grep /^_test_/,
      $stash->list_all_symbols('CODE');

    for my $test (@tests) {
        subtest $test => sub {
            try { $class->$test() }
            catch ($e) { diag $e }
        };
    }
    done_testing();
    return;
}

sub _test_can ($class) {
    state @subs = qw(run call_method methods test);
    can_ok $class, \@subs, "can do: @subs";
    return;
}

sub _test_methods ($class) {
    my $mock = mock 'WebService::FOAAS' => ( track => 1 );

    for my $method ( $class->methods() ) {
        $mock->override( $method => 1 );

        ok lives { $class->call_method($method) },
          "$method lives";
        ok scalar $mock->sub_tracking->{$method}->@*,
          "$method called";
    }
    return;
}

sub _test_service_failure ($class) {
    my $mock = mock 'WebService::FOAAS';

    for my $method ( $class->methods() ) {
        $mock->override( $method => sub { die 'mocked' } );

        my $exception =
          dies { $class->call_method($method) };
        isa_ok $exception, [ServiceError],
          "$method throws ServiceError on failure";
        like $exception->message, qr/^mocked/,
          "correct error in $method exception";
    }
    return;
}

1;
```

_[Updated, thanks to [Dan Book](http://www.grinnz.com/), [Karen Etheridge](https://github.com/karenetheridge), and [Bob Kleemann](https://metacpan.org/author/BOBK)]_ The only goofy bit above is the need to put the exception calls in a `BEGIN` block and then explic­it­ly call `BEGIN { Local::CallFOAAS::Exceptions->import() }`. Since the two pack­ages are in the same file, I can’t do a `use` state­ment since the implied [`require`](https://perldoc.perl.org/functions/require) would look for a cor­re­spond­ing file or entry in [`%INC`](https://perldoc.perl.org/variables/%25INC). (You can get around this by mess­ing with `%INC` direct­ly or through a mod­ule like [me::inlined](https://metacpan.org/pod/me::inlined) that does that mess­ing for you, but for a single-​purpose mod­uli­no like this it’s fine.)

* * *

[Do you like what you see? Leave a tip!](https://phoenixtrap.com/tip)