http://qs1969.pair.com?node_id=423402

legato has asked for the wisdom of the Perl Monks concerning the following question:

I am writing a module to support an internal project, and I've come to the point where I need to design the error system. In general, when module subroutines encounter an error, they must return the error string (which is actually specified outside my control) so that the calling code can decide what action to take for that specific error.

The error strings are modelled after network protocol communication, i.e. they begin with a number, a space, and then contain the error text '220 Cannot connect to service'. The programming model I generally use is "Return true on success, False on failure". Obviously (as I confirmed in a CB session with tye,theorbtwo and others) I cannot return error strings that are logically false, but I'd like to preserve the illusion. ;-)

Imagine code looking somewhat like this:

unless ( $object->method('param') ) { ## somehow get $errstr my ($errcode) = split ' ', $errstr; &$erract{$errcode}($errstr); } ## %erract contains such things as ('220' => sub { die $_ });

My debate comes in the 'somehow get $errstr' area. I could do what DBI does, and set $Module::errstr, but I've always personally disliked that approach, and the development section rules here strongly eschew globals in "includes" (It's not a Perl shop, so I interpret that to be "modules" for my purposes).

Ideally, the above chunk of code would leave the error string in $_, so that this would work:

unless ( $object->method('param') ) { my ($errcode) = split ' ', $_; &$erract{$errcode}($_); }

The closest way I can think of to implement that is to use die to throw and catch exceptions, like:

eval { $object->method('param') }; if ($@) { my ($errcode) = split ' ', $@; &$erract{$errcode}($@); }

But, I think I may have screwed myself out of being able to do that -- I use exceptions for internally fatal errors, and I am loathe to call any of the spec errors "fatal".

Is there another way to use this general approach to dealing with errors? I'm looking to end up with calling code that is clean and understandable, even for Perl neophytes; but, of course, it's most important that it works well.

Thanks in advance for any ideas, links, reading resources, etc. related to this.

Update {

Thanks to dragonchild and monkey_boy, I have solved the problem by extending Class::Base, which roughly implements dragonchild's suggestion. Thank you to all the monks who helped me reason this one out!
}

Anima Legato
.oO all things connect through the motion of the mind

Replies are listed 'Best First'.
Re: Handling non-fatal method error strings without exceptions or globals?
by dragonchild (Archbishop) on Jan 19, 2005 at 15:15 UTC
    What's wrong with $object->get_error? This way you can have object-specific errors.

    Obviously (as I confirmed in a CB session with tye,theorbtwo and others) I cannot return error strings that are logically false, but I'd like to preserve the illusion.

    That's not entirely true. You can use overload to create an object that will return a string when stringified and false when boolified. And, frankly, that's what I'd do if you're not going to allow exceptions or globals, but still require an error string.

    Being right, does not endow the right to be rude; politeness costs nothing.
    Being unknowing, is not the same as being stupid.
    Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
    Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

      I read through the overload docs, and while I understand bascially what it does, I can't seem to grok how to use overload to deal with this case. If you could provide a short example block of code, it would be most helpful to me.

      Anima Legato
      .oO all things connect through the motion of the mind

        # This code has been run through a basic smoke-test. package My::Error::String; use overload '""' => \&stringify, '0+' => \&boolify, fallback => 1; sub new { my $class = shift; my $error = shift; bless \$error, $class; } sub stringify { my $self = shift; return $$self; } sub boolify { return; }

        Then, when you want to use it, you'd do something like:

        use My::Error::String; sub func_with_error { if ($have_error) { return My::Error::String->new( "Have an error somewhere ..." ) +; } }

        Being right, does not endow the right to be rude; politeness costs nothing.
        Being unknowing, is not the same as being stupid.
        Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
        Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

        Just off the top of my head, I would do something like this:

        package Result; use overload 'bool' => \&as_bool, '""' => \&stringify; sub new { if ($_[0] eq __PACKAGE__) { shift; } my $self = { as_bool => $_[0], string => $_[1], }; bless $self, __PACKAGE__; $self; } sub as_bool { my $self = shift; $self->{as_bool}; } sub stringify { my $self = shift; $self->{string}; } 1;

        Then you can use it like this:

        require Result; sub complex_function { # ... if ($error_message) { return Result->new(undef, $error_message); } return Result->new(1); }

        With this, the caller wouldn't change much at all:

        unless (my $r = complex_function()) { print "Error received: $r\n"; }

        Hope that helps. (Completely untested ;->) Also, the way it is now, Result is not a class you can easily derive from ... I leave that as an excersise for other monks ;->

Re: Handling non-fatal method error strings without exceptions or globals?
by itub (Priest) on Jan 19, 2005 at 16:19 UTC
    Another option is to have your functions return multiple values:
    my ($result, $error) = function($arg); if ($error) { # do something... }

    One caveat is that if for some reason you want to disregard the error and only want the result, you need to remember to use parentheses:

    ($result) = function($arg); # get the result only $result = function($arg); # WRONG!!! gets the error only
      Or, you could use wantarray to eliminate the caveat...
      #!/usr/bin/perl -w $result = fun(); print "$result\n"; ($result, $error) = fun(); print "$result, $error\n"; sub fun { return wantarray ? ("stuff","Error") : "stuff" }


      -- All code is 100% tested and functional unless otherwise noted.
Re: Handling non-fatal method error strings without exceptions or globals?
by demerphq (Chancellor) on Jan 19, 2005 at 17:58 UTC

    You could use dualvar and a hash...

    use Scalar::Util qw(dualvar); my %Error=(220=>"Cannot connect to service"); sub mysub { if (@_) { return dualvar(0,1); } else { return dualvar(220,0); } } my $x; $x=mysub() or die $Error{$x};

    Of course this still leaves the problem of where to put the error, in the return or in a global. I think your options range from using something like $Pack::Error (which is IMO infinitely preferable to using the magic global $_), using $@ or using something like ${^MYERROR}. For instance I think the following is a reasonable approach:

    use Scalar::Util qw(dualvar); my %Error=(220=>"Cannot connect to service"); sub mysub { if (@_) { $@=dualvar(0,1); } else { $@=dualvar(220,0); } return $@ } mysub() or die sprintf "%d %s",$@,$Error{0+$@};

    Assuming that you dont have any eval checking logic that this will interfere with (and im not seeing immediately how that could happen anyway) then it should be ok.

    ---
    demerphq

Re: Handling non-fatal method error strings without exceptions or globals?
by monkey_boy (Priest) on Jan 20, 2005 at 13:25 UTC
    I use the error system suggested by dragonchild , already implemented in Class::Base

    I should really do something about this apathy ... but i just cant be bothered