There's a lot to love about Perl 5.14, but one of the best changes is a subtle one: Native exception handling is now reliable.
Perl's native exception syntax uses an eval block as the traditional "try" block, and requires checking
$@
to see if it contains an exception.
eval {
...; # do something that might throw an exception
};
if ($@) {
...; # handle exception here
}
Just like other languages code in the block is evaluated until an exception is thrown and then control jumps out of the block. Perl doesn't have a native
catch
syntax however - it simply puts the exception into
$@
where you can check it with a standard if statement.
Herein lies the problem in all versions of Perl prior to 5.14. Prior to 5.14
$@
is assigned and then the block is immediately exited; With 5.14 the block is immediately exited and then
$@
is assigned. A subtle, but important difference.
Perl's improvised catch mechanism
relies on
eval
undefining
$@
if no exception occurred (so the
if
block won't execute.) Each time Perl executes an eval therefore it must undefine $@. Prior to 5.14 this interacts
badly with object destructors.
package SomethingComplex;
sub new { return bless {}, shift };
sub DESTROY {
eval {
...; # some cleanup code that might throw an exception
};
if ($@) {
...; # handle exception in cleanup code
}
}
package main;
eval {
my $thingy = SomethingComplex->new();
...; # do something that might throw an exception
};
if ($@) {
...; # will never be executed even on exception
}
If an exception occurs in the eval block in main then execution will stop and control will immediately jump out of the block.
$thingy
will fall out of scope. When this happens the object's
DESTROY
block will be executed. This in turn runs its own
eval
which will unset
$@
as it executes. Assuming another exception doesn't occur during cleanup by the time we reach the
if
statement in main
$@
will have been undefined even though an exception happened in the
eval
block immediately above. Disaster!
The, simple, quite frankly terrible, workaround is to write this:
eval {
...; # do something that might throw an exception
1;
} or do {
...; # handle the exception here
}
We're no longer relying on the
$@
to tell that an exception has occurred but on the fact that an eval block quill return false on exception handling. Of course, we can't now reliably look at
$@
to find out what kind of exception occurred. There's ways around this, but they're even more complex to code.
A better fix on Perl's prior to 5.14 is to use the Try::Tiny module from the CPAN that handles all of this for us.
use Try::Tiny;
try {
...; # do something that might cause an exception.
} catch {
...; # handle the exception stored in $_ here
};
Of course, no matter how tiny Try::Tiny is, there's no getting away from the fact that it's not a module that's bundled with Perl; I can rely on getting it installed whenever I install software, but not on every machine I might happen to admin and want to make use of the system perl.
Luckily, Perl 5.14 solves this problem entirely for us by executing the block first - therefore executing all destructors that might mess with
$@
first - and then, once all that's done, populating
$@
with the exception.
Thanks Perl 5 Porters!