when to localize $@?

rjbs on 2008-08-29T00:42:20

Today I spent a good while trying to figure out why I wasn't seeing a runtime error from code that looked like this (grossly simplified):

eval {     ...     $object->method_that_doesnt_exist;     ...   };   if (my $error = $@) {     log($error);   }

Fortunately, this is something like the third time I've encountered this error in the past six months, and I am now quick to guess at it. Some of the code in the eval created an object that had a DESTROY handler, and it threw an exception without first localizing $@. This clobbered the real exception, so by the time the eval block was exited, $@ was empty. Ugh!

I learned my lesson the first time: I'm careful, now, to make sure my own DESTROY methods localize $@, because they can be invoked when I least expect it.

Where else does one have to remember to be Really Careful? Is there anything that provides sufficient sugar to make this painless?


It's a well-known problem

Ed Avis on 2008-08-29T09:36:27

Yes, this is a well-known problem with eval. You cannot, as the manual page suggests, do eval { ... } and then test $@ to see whether there was an error. The correct incantation is something like

my $r = eval { ... };
my $error = $@;
$error //= 'unknown error' if not $r;
if ($error) { ... }

By well-known I mean known to a tiny minority of cognoscenti, while most perl programmers and the documentation are entirely ignorant of it. Which is the default situation for any trap or gotcha in the perl language. But I digress...

The Error module on CPAN provides syntactic sugar 'try', 'catch', and 'except'. It handles the case of eval {} in DESTROY half-correctly: an exception is thrown, but the original message is lost. See <http://rt.cpan.org/Ticket/Display.html?id=38836>.

The good news is that if you use Error everywhere and have try {} in your destructors instead of eval {}, it appears to work correctly.

Re:It's a well-known problem

Ed Avis on 2008-08-29T09:37:57

Duuh, I meant

my $r = eval { ...; 1 };
my $error = $@;
$error //= 'unknown error' if not $r;
if ($error) { ... }

Note the trailing '1' in the eval block.

(It doesn't help that the comment engine here is doing something weird so you lose all your text if you go Back and Forward in Firefox. And those submit buttons are not buttons at all but look like links. What's going on?)

Re:It's a well-known problem

rjbs on 2008-08-29T10:48:44

Yeah, I generally do that, and definitely suggest that anyone else do it, too. What I was wondering about, though, is prevention. People are going to *forget* to do that, and if my destructors localize $@, they won't clobber it. Where else is there clobbering potential?

I'm not even sure the destructor in quesiton did call eval. I guess maybe somewhere down *its* calls. Blech!

Re:It's a well-known problem

Ed Avis on 2008-08-29T11:43:17

For prevention, you just have to run perlcritic over your code. If you're calling other people's code that isn't perlcritic-clean, then 'local $@' in every DESTROY method might save you. I suppose there should be a perlcritic policy for that too.

I don't like the way that the perl core has all sorts of traps for the unwary and can seemingly never be fixed for fear of breaking backwards compatibility, but perlcritic provides a useful sticking plaster.

When? Simple:

Aristotle on 2008-08-29T11:19:58

Always.

If you use eval, there has to be a local $@ somewhere nearby. There is no good rule of thumb for where exactly because that depends on the structure of your code. But you have to make sure $@ is restored at some point before execution returns from the scope you are controlling. That is the key: do not pass control back to your caller without fixing $@ if you used eval.

Devel::EvalError

clintongormley on 2008-08-30T13:40:50

You may also want to look at a module recently release by Tye McQueen:

Devel::EvalError

http://search.cpan.org/user/tyemq/Devel-EvalError-0.001002/EvalError.pm

clint