A colleague encountered an odd problem the other day. It took us a while to realise what was going on, you'll probably work it out much quicker ...
Here's a simplified version of what we were dealing with:
use strict; use warnings; eval { my $obj = SomeClass->new(); die "Code Red!\n"; }; if($@) { print "Exception caught: $@\n"; } else { print "No exception occurred\n" }
We both expected it to output:
Exception caught: Code Red!
but instead we got:
No exception occurred
You probably worked it out straightaway but for the record, here's what was happening ...
The die statement did generate an exception and the message was stored in $@. Of course the code in the eval block stopped executing immediately but when control flow exited the block, any lexcial variables declared in the block went out of scope. In this case there was only one: $obj. What we didn't realise is that $obj's class defined a destructor method ('DESTROY'). This method is called when the object goes out of scope and apparently in our case the method wrapped something in an eval. Whatever that something was, it completed without generating an exception, so $@ ended up empty - indicating no error and overwriting the exception message that our code put there.
So, if you write a destructor method and it needs to use eval, please remember to use local($@) to restore the previous value of $@ when your routine completes.
Cross-posted from my shiny new blog
The problem of needing to localise special variables is discussed in http://perldoc.perl.org/perlobj.html#Destructors.
There is a P::C policy ErrorHandling::RequireCheckingReturnValueOfEval that catches this programming mistake. Its documentation also offers an alternative approach where you don't need to mess with other people's destructors to make it work.
package SomeClass;
sub new {
return bless \my $foo => __PACKAGE__;
}
sub DESTROY {
eval {'resetting $@'};
}
package main;
use strict;
use warnings;
if (
eval {
my $obj = SomeClass->new();
die "Code Red!\n";
1;
}
) {
print "Exception caught: $@\n";
} else {
print "No exception occurred\n";
}
Re:critic to the rescue
davebaker on 2009-02-03T05:36:26
Did you mean to say
if (
eval {
my $obj = SomeClass->new();
die "Code Red!\n";
1;
}
) {
print "No exception occurred\n";
} else {
print "Exception caught: $@\n";
}
Re:How?
Aristotle on 2009-01-31T17:27:46
Err. How what?
local $@;
Just like he said.
Following the eval block with an "or" seems to be more reliable than using "if ($@)
" to check for an exception in an eval block, because an eval block (or even an eval EXPR) returns undef if there is a syntax error or runtime error or a die statement is executed (according to perldoc -f eval).
eval {
my $obj = SomeClass->new();
die "Code Red!\n";
} or print "Exception caught: $@\n";
But even with this technique the $@ is reset by the destructor in your example, so while the exception at least can be caught we lose the information $@ would provide but for the destructor. The output is merely
"Exception caught:"
The documentation for eval says that $@ is guaranteed to be a null string if no exception occurred; I had not appreciated the precise scope of that statement until seeing your example (it doesn't go on to say that, conversely, $@ is guaranteed not to be a null string if an exception occurred).
I believe I heard schwern advocating at YAPC 2008 the use of
eval {
or warn_about_problem();
rather than
eval {
if ($@) { warn_about_problem() }
This might have been the kind of situation he had in mind.