-
-
06 Jul 2008 23:08:59 UTC
- Distribution: Devel-EvalError
- Module version: 0.001002
- Source (raw)
- Browse (raw)
- Changes
- How to Contribute
- Issues (1)
- Testers (856 / 0 / 0)
- Kwalitee
Bus factor: 0- 69.50% Coverage
- License: unknown
- Activity
24 month- Tools
- Download (7.83KB)
- MetaCPAN Explorer
- Permissions
- Subscribe to distribution
- Permalinks
- This version
- Latest version
- Dependencies
- unknown
- Reverse dependencies
- CPAN Testers List
- Dependency graph
NAME
Devel::EvalError -- Reliably detect if and why eval() failed
SYNOPSIS
use Devel::EvalError(); my $ee = Deval::EvalError->new(); $ee->ExpectOne( eval { ...; 1 } ); if ( $ee->Failed() ) { # if ( ! $ee->Succeeded() ) ... $ee->Reason() ...; }
DESCRIPTION
Although it is common to check
$@
to determine if a call toeval
failed, it is easy to makeeval
fail while leaving$@
empty.Using
Devel::EvalError
encourages you to use more reliable ways to check whethereval
failed while also giving you access to the failure reason(s) even if$@
ended up empty. (It also makes$@
ending up empty less likely for other uses ofeval
.)If you have code that looks like the following:
eval { ... }; if ( $@ ) { log_failure( "...: $@" ); }
Then you should replace it with code more like this:
use Devel::EvalError(); # ... my $ee = Devel::EvalError->new(); $ee->ExpectOne( eval { ...; 1 } ); if ( $ee->Failed() ) { log_failure( "...: " . $ee->Reason() ); }
Caveats
It is important to call
Devel::EvalError-
new()> before doing theeval
. Although I believe that in all existing implementations of Perl v5, the following code still works, there is no iron-clad guarantee that it will do things in the required order (such as in some future version of Perl). So you might not want to risk using it:my $ee = Devel::EvalError->new()->ExpectOne( eval $code . "; 1" );
It is important that the Perl code that you evaluate ends with an expression that returns just the number one. When evaluating a string, append
"; 1"
to the end of the string. When evaluating a block, add; 1
to the end inside the block, like so:$ee->ExpectOne()->( eval { ...; 1 } );
If the
eval
'd code returns early, it is important that it does so either viareturn 1;
or bydie
ing.Since you can't rely on
$@
to tell ifeval
failed or succeeded, you need to rely on whateval
returns.eval
indicates failure by returning an empty list so it is very important to not doreturn;
inside theeval
(of course,return;
in some subroutine called from youreval
'd code is not a problem). You also should avoidreturn @list;
unless you can be certain that@list
is not empty.ExpectOne()
requires thateval
either returns an empty list or returns just the number one (otherwise itcroak
s).Why
$@
is unreliableIt is a bug in Perl that the value of
$@
is not guaranteed to survive untileval
finishes returning. This bug has existed since Perl 5 was created so there are a lot of versions of Perl around where you can run into this problem. Here is a quick example demonstrating it:my $ok = eval { my $trigger = RunAtBlockEnd->new( sub { warn "Exiting block!\n" }, ); die "Something important!\n"; 1; }; if( $@ ) { warn "eval failed ($@)\n"; } elsif( $ok ) { warn "eval succeeded\n"; } else { warn "eval failed but \$@ was empty!\n"; } { package RunAtBlockEnd; sub new { bless \$_[-1], $_[0] } sub DESTROY { my $self = shift @_; eval { ${$self}->(); 1 } or warn "RunAtBlockEnd failed: $@\n"; } }
This code produces the following output:
Exiting block! eval failed but $@ was empty!
The crux of the problem is the use of
eval
inside of aDESTROY
method while not also doinglocal $@
in that method. Note that it is also a problem if any code called, however indirectly, from aDESTROY
method useseval
withoutlocal $@
, so preventing the problem can be quite difficult (and once you have identified that this problem is happening to you, the inability to overloadeval
prevents easily finding the source of the problem).Note that the use of
Devel::EvalError
also has the side-effect of localizing the changing of$@
so it not only works around this problem if used on the outereval
, it would also prevent the problem if only used on the innereval
. If we change ourDESTROY
method to:sub DESTROY { use Devel::EvalError(); my $self = shift @_; my $ee = Devel::EvalError->new(); $ee->ExpectOne( eval { ${$self}->(); 1 } ); warn "RunAtBlockEnd failed: ", $ee->Reason(), "\n" if $ee->Failed(); }
Then our snippet produces the following results:
Exiting block! eval failed (Something important! )
Why
use
notrequire
?Note that we wrote
use Devel::EvalError();
and notrequire Devel::EvalError;
in the above contrived example. That is because, the first time a module isrequire
'd, the code for the module has to beeval
'd, which also clobbers$@
just like a straighteval
would. So doing arequire
inside of aDESTROY
method causes the same problem.So all of our examples use
use Devel::EvalError();
just in case somebody pastes some example code into theirDESTROY
method. In most real-world code, therequire
would be placed outside of theDESTROY
method and so is unlikely to cause a problem. So if you preferrequire
overuse
in some cases, you can usually writerequire Devel::EvalError;
with no problem.Methods
new
new()
is a class method that takes no arguments and returns a newDevel::EvalError
object. You usually callnew()
like so:my $ee = Devel::EvalError->new();
new()
saves away the current value of$@
so that it can restore it when you are done using the returnedDevel::EvalError
object.new()
also sets up a$SIG{__DIE__}
handler to make a note of any exceptions that get thrown (such as by callingdie
). This "die handler" will also call the previous handler (if there was one) and the previous handler will be automatically restored later.ExpectOne
$ee->ExpectOne( eval { ...; 1 } ); $ee->ExpectOne( eval $code . '; 1' );
ExpectOne()
should be passed the results of a call toeval
. The code beingeval
'd should exit only by returning just the number one or by throwning an exception (such as by callingdie
).ExpectOne()
returns the object that invoked it so that you can use the following shortened form:my $ee = Devel::EvalError->new()->ExpectOne( eval ... );
But be aware that this shortened form relies on a particular order of evaluation that is not guaranteed. So you may wish to avoid this risk or just prefer to not rely on undefined evaluation order as a matter of principle.
If
ExpectOne()
gets passed just the number one, then theeval
succeeded, setting what several other methods will return.If
ExpectOne()
gets passed the empty list, then theeval
failed, setting the return values for other methods differently.The current release also interprets a single undefined value as
eval
having failed. This is to account for a use-case similar to:my $ee = Devel::EvalError->new(); my $ok = eval { ...; 1 }; $ee->ExpectOne( $ok );
But this interpretation may be subject to change in a future release of
Devel::EvalError
(to be treated the same as the following case).Being passed any other value will cause
ExpectOne()
to "croak" (see theCarp
module), reporting that the module has been used incorrectly.If
ExpectOne()
gets passed the empty list, then the value of$@
is immediately checked. If$@
is not empty, then its value is saved as the failure reason (other failure reasons may have been collected by the "die handler", but those will mostly be ignored in this case).ExpectOne()
also restores the previous "die handler" (if any).Reason
Reason()
returns either the empty string or a string (or object) containing (at least) the reason that the earliereval
failed. If it is unclear which of several different reasons actually caused theeval
to fail, then a string will be returned containing all of the possible reasons in chronological order.To simplify some coding cases,
Reason()
will safely return an empty string if called on anErase()d
object or one whereExpectOne()
has not yet been called [norExpectNonEmpty()
].AllReasons
AllReasons()
returns the list of strings and/or objects that repesent exceptions thrown between when our object was created and whenExpectOne()
was called [orExpectNonEmpty()
], in chronological order.Usually the last reason returned is the reason that the
eval
failed.Note that if a
DESTROY
method tries to throw an exception (a rather pointless thing to do unless the exception is caught within the DESTROY method), then the real reason for theeval
failing can have other reasons after it in the returned list of reasons. If that DESTROY method also didlocal $@;
(or equivalent) such that$@
was still properly set aftereval
finished failing, then the last reason returned will be the real reason why theeval
failed; that reason will just appear in the list of reasons twice.Succeeded
Succeeded()
returns a true value if the earliereval
succeeded. It returns a false value if the earliereval
failed. Otherwise it "croaks" (ifExpectOne()
has not yet been called or the invoking object has beenErase()
d, etc.).Failed
Failed()
returns a true value if the earliereval
failed. It returns a false value if the earliereval
succeeded. Otherwise it "croaks".Reuse
Reuse()
cleans up an existingDevel::EvalError
object and then prepares it to be used again. The following two snippets are equivalent:undef $ee; $ee = Devel::EvalError->new(); # Same as $ee->Reuse();
Note that you should not re-use a variable by simply puting a new
Devel::EvalError
object over the top of a previous one. Don't ever write code like the line marked "WRONG!" below:my $ee = Devel::EvalError->new(); # ... $ee = Devel::EvalError->new(); # WRONG! my $e2 = Devel::EvalError->new(); # ... $e2->Reuse(); # RIGHT!
Here is a quick example of how badly that can go wrong:
my $ee = Devel::EvalError->new(); if ( $DoStuff ) { $ee->ExpectOne( eval { do_stuff(); 1 } ); # ... } $ee = Devel::EvalError->new();
The above code produces output like:
$SIG{__DIE__} changed out from under Devel::EvalError at ... Devel::EvalError::_revertHandler... Devel::EvalError::Erase... Devel::EvalError::DESTROY... ... $SIG{__DIE__} changed out from under Devel::EvalError at ... Devel::EvalError::_revertHandler... Devel::EvalError::Erase... Devel::EvalError::DESTROY... ...
This is because the second
Devel::EvalError
object is created before the first one gets destroyed. The lifetimes ofDevel::EvalError
objects must be strictly nested or else they can't properly deal with sharing the single global$SIG{__DIE__}
slot.Calling
$ee-
Reuse();> ensures that the previous object gets cleaned up before the next one is initialized, preventing such noisy problems.Note that
Reuse()
returns the invoking object so that you can choose to use the following shortened form, despite the fact that it relies on a particular (undefined) order of evaluation:$ee->Reuse()->ExpectOne( eval ... );
Erase
Erase()
cleans up and clears out aDevel::EvalError
object. The below two snippets are equivalent:my $ee = Devel::EvalError->new(); # ... # eval() 1 undef $ee; # ... # non-eval() code $ee = Devel::EvalError->new(); # ... # eval() 2 my $ee = Devel::EvalError->new(); # ... # eval() 1 $ee->Erase(); # ... # non-eval() code $ee->Reuse(); # ... # eval() 2
Notice how using
Erase()
leaves the$ee
variable holding an object so you can just use$ee->Reuse()
rather than having to repeat the whole module name in order to callnew()
.Note also that
$ee->new()
is not allowed. If you don't want to re-type the module name and you want to use one object to create another separate object, then you can useref($ee)->new()
. But remember that you need to ensure that the lifespans ofDevel::EvalError
objects are strictly nested.The following contrived example shows how not being explicit with the nesting of the lifespans of
Devel::EvalError
objects can be a problem:{ my $e1 = Devel::EvalError->new(); my $e2 = ref($e1)->new(); # Both $e1 and $e2 get destroyed here ... # in what order? }
The above code produces two
$SIG{__DIE__} changed out from under Devel::EvalError ...
complaints. You can fix it as follows:
{ my $e1 = Devel::EvalError->new(); { my $e2 = ref($e1)->new(); # Only $e2 is destroyed here } # Only $e1 is destroyed here }
Sadly, the above contrived example may still give the annoying warnings due to a rare appearance of Perl 5 optimizations. Adding just one line of useless code prevents the optimization and the warnings. In real code, this optimization problem is much less likely to appear.
{ my $e1 = Devel::EvalError->new(); { my $e2 = ref($e1)->new(); # Only $e2 is destroyed here } my $x= "You may need code here to thwart optimizations"; # Only $e1 is destroyed here }
ExpectNonEmpty
You should probably not use the
ExpectNonEmpty()
method.No, really. Just go read some other section of the manual now.
Are you still here? Okay, since I wrote it, I guess I'll let you read the documentation about it as well.
ExpectNonEmpty()
can be used to useeval
to return an interesting value. For example:my $ee = Devel::EvalError->new(); my @list = $ee->ExpectNonEmpty( eval { getListDangerously() } );
But you really shouldn't do it that way. You should do it this way instead:
my $ee = Devel::EvalError->new(); my @list; $ee->ExpectOne( eval { @list = getListDangerously(); 1 } );
For one thing, if
getListDangerously()
returned an empty list, then much confusion would likely ensue.For another, scalar context isn't preserved when changing code from:
my $return = eval ...;
to:
my $return = $ee->ExpectNonEmpty( eval ... );
In the second line above, the
eval
is called in a list context. That code would be better written like:my $return; $ee->ExpectOne( eval { $return = ...; 1 } );
Or, in the case of
eval
'ing a string of Perl code:my $return; $ee->ExpectOne( eval "\$return = $code; 1" );
CONTRIBUTORS
Original author: Tye McQueen, http://perlmonks.org/?node=tye
LICENSE
Copyright (c) 2008 Tye McQueen. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
SEE ALSO
The Troll Under the Bridge, Fremont, WA
Module Install Instructions
To install Devel::EvalError, copy and paste the appropriate command in to your terminal.
cpanm Devel::EvalError
perl -MCPAN -e shell install Devel::EvalError
For more information on module installation, please visit the detailed CPAN module installation guide.