Search code examples
perlexceptiontry-catch

Perl unexpected behavior: croak vs. try catch


I had seen some exceptions that pointed to (end of) the catch block itself (see the example below).

As my opinion, this is an unexpected behavior, since it alters the location of original exception and make difficult to debug (it should say die at line 13.)

It shows the (correct) line 13, if I use die/confess or using eval instead try-catch.

Not knowing how my code will be called within the stack, I started to avoid using croak now. What do you think? Did I get right, or there is a way to improve this?

Best regards, Steve

use Carp;
use Try::Tiny;

try {
  foo();
} 
catch {
  # do something before die
  die $_;
};             # this is line 10

sub foo {
  croak 'die'; # this is line 13
}

Output: die at line 10.


Solution

  • This is the intended behavior from Carp

    [...] use carp() or croak() which report the error as being from where your module was called. [...] There is no guarantee that that is where the error was, but it is a good educated guess.

    So the error is reported at where the module's sub is called, which is what the user wants

    use warnings;
    use strict;
    use feature 'say';
    
    use Try::Tiny;
    
    package Throw {
        use warnings;
        use Carp qw(croak confess);
    
        #sub bam { die "die in module" };     # l.11 
        sub bam { croak "croak in module" };  
        1;  
    };
    
    try {
        Throw::bam();    # l.17
    }
    catch {
        say "caught one:   $_"; 
        die "die in catch: $_";
    };    
    say "done";
    

    Prints

    caught one:   croak in module at exceptions.pl line 17.
    
    die in catch: croak in module at exceptions.pl line 17.
    

    If the sub throws using die then this is reported at line 11, what is the normal behavior of die, and what you seem to expect.

    If any of this is unclear or suboptimal then better use confess and nicely get a full stacktrace. Also, if you wish more exception-based-like code behavior, can put together an exception/error class and throw its object, designed and populated as desired.

    If you want to confess an object note that at this time Carp has limits with that

    The Carp routines don't handle exception objects currently. If called with a first argument that is a reference, they simply call die() or warn(), as appropriate.

    One way then would be to confess a stringification of the object, getting at least both a full stack backtrace and whatever is in the object.


    I get the same behavior with eval, by replacing try-catch and $_ above

    eval { 
        Throw::bam();
    };
    if ($@) { 
        say "caught one:   $@"; 
        die "die in catch: $@";
    };
    

    Prints exactly the same as above


    While the above is clear and behaves as expected, a weird thing is indeed seen in the question's example: the error is reported from the whole try-catch statement, ie. at its closing brace, where line 10 is. (The try sub is prototyped and the whole try-catch is a syntax aid equivalent to a call to try that takes an anonymous sub, and then perhaps more. See ikegami's comment, and docs. Also see this post for more about its syntax.)

    This is strange since the call to the croaking sub is foo() inside the try statement and this line should be reported, what can be confirmed by running the script with -MCarp::Always. But in the code in this answer the line of the call to Throw::bam is indeed reported -- why this difference?

    The clear purpose of croak is to be used in the libraries, so that the user can see at which point in their (user's) code they called the library in a way that triggered an error. (While die would point to the place where error is detected, so in the library, most likely useless to the user. But read die and Carp docs for related complexities.)

    What isn't obvious is that when croak is emitted in the same namespace (main::foo()) from try-catch in its own namespace (Try::Tiny) things get confused, and the end of its statement is reported. This can be checked by adding a foo() to my code above and calling it (instead of a sub from a module), and we get the question's behavior reproduced.

    This doesn't happen if main::foo() with croak inside is called from a (complex) statement right in main::, so it seems to be due to the try-catch mix up of namespaces. (On the other hand, try-catch sugar adds an anonymous sub to the callstack, and this sure can mix things up as well.)

    In practical terms, I'd say: always use croak out of modules (otherwise use die), or, better yet if you want to mimic exception-based code, use confess and/or your exception class hierarchy.


    Even just like die ExceptionClass->new(...);

    Bear in mind that in the way of exceptions Perl only has the lonesome die, and eval. For more structure you'll need to implement it all, or use frameworks like Exception::Class or Throwable


    By writing and using a method that generates a plain string with useful information from the object, for Carp::confess $obj->stringify.

    Or by overloading the "" (quote) operator for the class since it gets used when confess-ing an object (string context), for Carp::confess $obj; this is good to have anyway.

    A basic example for both:

    use overload ( q("") => \&stringify );
    
    sub stringify { 
        my $self = shift; 
        join ", ", map { "$_ => " . ( $self->{$_} // 'undef' ) } keys %$self 
    }
    

    where instead of a reference to a named sub on can directly write an anonymous sub.