Exception::Stringy - Modern exceptions for legacy code

cross-posted from dams blog

Exception::Stringy - Modern exceptions for legacy code

A small recap of Perl exceptions

Basic Usage Of Exceptions

In Perl, exceptions are a well known and widely used mechanism. It is an old feature that has been enhanced over time. At the basic level, exceptions are triggered by the keyword die. Exceptions were initially used as a way to stop the execution of a program in case of a fatal error. The too famous line:

open my $fh, $file or die "failed to open '$file', error: $!";

is a good example.

The original way to catch exceptions in Perl has a somewhat strange syntax, it's based on the eval keyword and the special variable $@:

eval { code_that_may_die(); 1; }
  or say "exception has been caught: $@"

Nowadays, exceptions are usually thrown using croak and friends, from the Carp module. It allows for a much better flexibility about where the exception seems to originate, and how to display the stack trace, if any.

Catching exceptions with eval is also supersed by try/catch mechanisms. The most used one is via the [Try::Tiny][try-tiny] module by Yuval Kogman and Jesse Luehrs, and goes like this:

try {
  croak "exception";
} catch {
  warn "caught error: $_";
};

Throwing Objects

The good thing about die (or croak), is that it's very easy to use, when given a string. It's perfect for using in scripts, or moderately big projects. However, for more features, or extensive usage of exceptions, then it's better to throw objects instead of strings, like this:

open $file or die MyExceptions::IO::File->new(
  filename => $file,
  error => $!
);

For this snippet of code to work, the MyExceptions::IO::File class has to be declared, its fields as well, and the it should probably inherit from MyExceptions::IO. So it requires some amount of work.

Some modules have been created - long time ago - to automate or help with declaring exception classes. The most well known one is [Exception::Class][exception-class], by Dave Rolsky. For instance, here is how to declare two exceptions matching with previous example:

package MyExceptions;

use Exception::Class (
'MyException::IO',
'MyException::IO::File' => {
isa => 'MyException::IO',
fields => [ 'filename' ],
},
);

And then, here is the code to make use of that and throw an exception when failing to open a file:

use MyExceptions;

open $file or MyException::IO::File->throw(
filename => $file,
error => $!
);

Catching Objects Exceptions

When using objects as exceptions, a set of features becomes available, thanks to Object Oriented Programming. Inheritance, attributes and introspection are some of them. However the most visible and used feature is about catching such exceptions:

use MyException;

try {
open $file or MyException::IO::File->throw(
filename => $file,
error => $!
);
} catch {
my $exception = $_;
if ($exception->isa(MyException::IO)) {
# we know how to handle these
} else {
$exception->rethrow
}
};

As you can see, it's easy to introspect an exception if it's an object. In this case we use the isa keyword to know if the exception is or inherits from a given class name.

When things go wrong

Mixing Objects And String Exceptions

As we saw in the previous chapter, Perl allows exceptions being whatever you like (string, objects, but actually numbers, structures, etc, work as well).

Usually, when starting a project, the author decides whether to use simple strings or objects with a class hierarchy. With very big projects, it is sometimes not possible to impose one kind of exceptions. This may be due to legacy code, a subproject that was included, or the wish to give people freedom about what they want to use depending on the context.

In these cases, the code may have to handle exceptions of two kinds: strings and objects. This can be done via this kind of code:

use MyException;
use Scalar::Util qw(blessed);

try {
# ... code that may die
} catch {
my $exception = $_;
if (blessed $exception) {
# exception is an object
# ...
} else {
# exception is a normal string
# ...
}
};

Mixed Exceptions Issues

The previous code snippet suffers from increased complexity due to the additional checks and two different codepaths for handling potential errors. This is clearly both suboptimal and error prone.

Another issue is that some code may consider that the exception it is catching is of one type, whereas it could be of an other type, especially because of the action-at-distance nature of the exception. Consider this function:

sub do_stuff {
    try {
        # ... code that can only throw objects exceptions
    } catch {
        my $exception = $_;
        # exception is always an object
        if ($exception->isa(...)) {
            # ...
        }
    };
}

This code assumes that the exception will always be an object. However, let's consider this: in following example, the function do_stuff is called (its original code is unchanged), but before doing so, the special signal handler for __DIE__ is changed.

$SIG{__DIE__} = sub { die "FATAL: $_[0]" };
do_stuff();

The first line of the example is being called when an exception is raised, and will be executed instead of propagating the exception. What this code does is prepending FATAL:to it, then propagate the exception again by using die.

Alas, it is doing so in a naive way, by forcing the exception (in $_[0]) to be evaluated as a string. So when the exception is then re-thrown, it is now a string ! and Boom, the ->isa call in do_stuff won't work.

The worst thing about this kind of issue is that it doesn't appear at compile time, nor at execution time, but at exception time, which is the worst time...

The Overloaded Stringification Route

So at that point, most developers will choose the following strategy. Use object exceptions for their code, but guard against receiving string exceptions, and also make their object exceptions nicely degrade into strings, by using stringification overloading. That means that if an object exception is managed by a handler that threats it as a string, the exception will transform itself into a string, and try to present some meaningful aspect of itself.

The issue is that handling exception is now back to square one, having to deal with strings, trying to parse it looking for meaningful information to hopefully make a good decision.

What if, instead of taking an object exception and downgrading it to a string while keeping as much information as possible, one starts from a string, and enhance it until it looks like an object, without being one ? That way we would have the best of both worlds

This is what Exception::Stringy tries to achieve.

Exceptions::Stringy from scratch

The Needed Features

A perfect exception would have these features:

  • be a string, containing an error message
  • be an instance of a class
  • be able to inherit from an other exception
  • have simple fields with values
  • provide a way to introspect itself

This set of features is not big, but it's probably enough for a start. Let's see how we can implement them in a simple string. We're going to use an exception with these attributes:

  • an error message 'permission denied'
  • from the class MyException::IO
  • which inherits from MyException
  • with a field filename

Class Instance

Let's start with the first feature: be a string, containing an error message. That's easy:

"permission denied"

Being an instance of a class is usually done in Perl by using bless on a ScalarRef. But we don't want the eception to be an object. What bless does - and what it ultimately means to "be an instance of a class", is just attaching a label to a value. Let's do that, by having a label as a substring in our exception. For instance:

"[MyException::IO]permission denied"

We could add a magic mark or have a more complex label syntax to make sure it's a legit label.

To know what the class of a given exception is, we just need to extract the label, for instance with a regex.

Class Inheritance

Inheritance is easy, it only requires that standard Perl classes be created to map the exception labels, and then Perl usual inheritance can be used.

So, following our example, we need two packages, MyException and MyException::IO, and @MyException::IO::ISA set to ['MyException']. This can be made automatically at exception declaration time.

Fields

For simplicity, Exception::Stringy only handles simple field values, that is strings and numbers basically. To put fields into our string, we need to be able to identify them, so for instance with a separator between the different fields, and an other one between a field name and its value. Like this:

"[MyException::IO|filename:/tmp/file|]permission denied"

And if the field name or value contains one of the separators ( [, |, : or ]), let's encode them in base64, and mark it as such.

So, by now, we have fleshed out a string with useful data, which is properly parseable, and can be described. Let's add methods to the data now.

Introspection and Modification

Given an exception, it is mandatory to be able to introspect and modify it, namely be able to:

  • get/set the class of the exception,
  • get/set the fields values attached to the exception,
  • get/set the exception message,
  • other useful methods.

In an ideal world, we would want methods, that we can call on our exception instances. However because our exceptions are regular strings, we can't do this:

$exception->message();

Usually, this way of calling a method (the arrow notation) works only if $exception is a blessed reference (that is, an object). However, there are other cases in which we can use the arrow notation, and have it work in a similar way. One of it is this one:

$exception->$message();

If $message is a variable that contains a reference on a subroutine, then the previous line will translate into:

$message->($exception);

And it works whatever the type of $exception, like in our case, a string. So, Exception::Stringy creates the needed subroutine references for the user and allow such arrow notation, which is very similar to the OO method invocation. I call these pseudo methods.

However, to avoid clobbering an existing variable, the pseudo methods need to have names that are unlikely to be already used in the target package. It's even better if there is an option to add a prefix to these pseudo-methods. Once again, Exception::Stringy provides these features. The default pseudo method names are :

$exception->$xthrow()
$exception->$xrethrow()
$exception->$xraise()
$exception->$xclass()
$exception->$xisa()
$exception->$xfields()
$exception->$xfield()
$exception->$xmessage()
$exception->$xerror()

Launching The Exception

Finally, once we have created the exception, let's throw it. The first think to do is to implement a throwor `raise class method on all the exception class, so that we can do

MyException->throw(...)

That will basically craft a new exception string, with all the properties encoded in it, and call die or croak on it.

We can also use a pseudo method on an existing exception to (re)throw it:

$exception->$xthrow();

Exceptions::Stringy example

Synopsis

Below is the synopsis of the Exceptions::Stringy module. It's basically a wrap up of what has been explained above. The exceptions definition is heavily inspired from Exception::Class.

use Exception::Stringy;
Exception::Stringy->declare_exceptions(
    'MyException',

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

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

### with Try::Tiny

use Try::Tiny;

try {
# throw an exception
MyException->throw('I feel funny.');

# or use an alias
throw_fields 'Error message', grandiosity => 1;

# or with fields
ExceptionWithFields->throw('I feel funny.',
quixotic => 1,
grandiosity => 2);

# you can build exception step by step
my $e = ExceptionWithFields->new("The error message");
$e->$xfield(quixotic => "some_value");
$e->$xthrow();

} catch {
if ( $_->$xisa('Exception::Stringy') ) {
warn $_->$xerror, "\n";
}

if ( $_->$xisa('ExceptionWithFields') ) {
if ( $_->$xfield('quixotic') ) {
handle_quixotic_exception();
} else {
handle_non_quixotic_exception();
}
} else {
$_->$xrethrow;
}
};

### without Try::Tiny

eval {
# ...
MyException->throw('I feel funny.');
1;
} or do {
my $e = $@;
# .. same as above with $e instead of $_
}

Conclusion

This was an in-depth look at why and how to build up a resilient and non-intrusive exception mecanism. I hope to have demonstrated one aspect of the extreme flexibility of Perl.

Feel free to use Exception::Stringy, it is being used in production code for some time now. Feedback welcome !

Links

Leave a comment

About Damien "dams" Krotkine

user-pic I blog about Perl.