views:

1734

answers:

3

I have a piece of Perl code somewhat like the following (strongly simplified): There are some levels of nested subroutine calls (actually, methods), and some of the inner ones do their own exception handling:

sub outer { middle() }

sub middle {
    eval { inner() };
    if ( my $x = $@ ) { # caught exception
        if (ref $x eq 'ARRAY') {
            print "we can handle this ...";
        }
        else {
            die $x; # rethrow
        }
    }
}

sub inner { die "OH NOES!" }

Now I want to change that code so that it does the following:

  • print a full stack trace for every exception that "bubbles up" all the way to the outermost level (sub outer). Specifically, the stack trace should not stop at the first level of "eval { }".

  • Not having to change the the implementation of any of the inner levels.

Right now, the way I do this is to install a localized __DIE__ handler inside the outer sub:

use Devel::StackTrace;

sub outer {
    local $SIG{__DIE__} = sub {
        my $error = shift;
        my $trace = Devel::StackTrace->new;
        print "Error: $error\n",
              "Stack Trace:\n",
              $trace->as_string;
    };
    middle();
}

[EDIT: I made a mistake, the code above actually doesn't work the way I want, it actually bypasses the exception handling of the middle sub. So I guess the question should really be: Is the behaviour I want even possible?]

This works perfectly, the only problem is that, if I understand the docs correctly, it relies on behaviour that is explicitly deprecated, namely the fact that __DIE__ handlers are triggered even for "die"s inside of "eval { }"s, which they really shouldn't. Both perlvar and perlsub state that this behaviour might be removed in future versions of Perl.

Is there another way I can achieve this without relying on deprecated behaviour, or is it save to rely on even if the docs say otherwise?

+5  A: 

It is not safe to rely on anything that the documentation says is deprecated. The behavior could (and likely will) change in a future release. Relying on deprecated behavior locks you into the version of Perl you're running today.

Unfortunately, I don't see a way around this that meets your criteria. The "right" solution is to modify the inner methods to call Carp::confess instead of die and drop the custom $SIG{__DIE__} handler.

use strict;
use warnings;
use Carp qw'confess';

outer();

sub outer { middle(@_) }

sub middle { eval { inner() }; die $@ if $@ }

sub inner { confess("OH NOES!") }
__END__
OH NOES! at c:\temp\foo.pl line 11
    main::inner() called at c:\temp\foo.pl line 9
    eval {...} called at c:\temp\foo.pl line 9
    main::middle() called at c:\temp\foo.pl line 7
    main::outer() called at c:\temp\foo.pl line 5

Since you're dieing anyway, you may not need to trap the call to inner(). (You don't in your example, your actual code may differ.)

In your example you're trying to return data via $@. You can't do that. Use

my $x = eval { inner(@_) };

instead. (I'm assuming this is just an error in simplifying the code enough to post it here.)

Michael Carman
Right, but I was hoping I could catch unforeseen errors with this (some of the inner levels might not be written by me). I'm beginning to think that the behaviour I want is actually not possible (See my edit of the question).
trendels
I understand and sympathize. Doing this externally requires dark magic. Sinan's answer is a good one if you can stomach overriding built-in functions. I'd have no qualms about doing so for debugging but would be hesitant to use it in production code. I *might* use it in a script but would *not* use it in a module.
Michael Carman
"In your example you're trying to return data via $@. You can't do that." Well, yes you can. Returning data is a bad idea (and is not what the OP is doing), but you can throw extra information about the exception. It is common to throw instances of classes and pattern-match on their type, for example. (I recommend the TryCatch module on CPAN, for extra syntax sugar in this case, however.)
jrockway
+6  A: 

UPDATE: I changed the code to override die globally so that exceptions from other packages can be caught as well.

Does the following do what you want?

#!/usr/bin/perl

use strict;
use warnings;

use Devel::StackTrace;

use ex::override GLOBAL_die => sub {
    local *__ANON__ = "custom_die";
    warn (
        'Error: ', @_, "\n",
        "Stack trace:\n",
        Devel::StackTrace->new(no_refs => 1)->as_string, "\n",
    );
    exit 1;
};

use M; # dummy module to functions dying in other modules

outer();

sub outer {
    middle( @_ );
    M::n(); # M::n dies
}

sub middle {
    eval { inner(@_) };
    if ( my $x = $@ ) { # caught exception
        if (ref $x eq 'ARRAY') {
            print "we can handle this ...";
        }
        else {
            die $x; # rethrow
        }
    }
}

sub inner { die "OH NOES!" }
Sinan Ünür
Hm, interesting. This actually does exactly what I had in mind. It also shows that I probably should rethingk my general approach. I don't really want to go so far as to override builtins. :(
trendels
I think you have to choose between changing inner() and changing what die() means.
darch
@trendels Well, if this is just going to be used for debugging, it might be OK to override die.
Sinan Ünür
+2  A: 

Note that overriding die will only catch actual calls to die, not Perl errors like dereferencing undef.

I don't think the general case is possible; the entire point of eval is to consume errors. You MIGHT be able to rely on the deprecated behavior for exactly this reason: there's no other way to do this at the moment. But I can't find any reasonable way to get a stack trace in every case without potentially breaking whatever error-handling code already exists however far down the stack.

Eevee