Exception Handling ... hmm

heusserm on 2003-09-17T11:33:50

Right now, I'm running into a lot of legacy code like this:

#------------------------# sub GetValues #------------------------# { my $dbh = get_connection('databasename'); my $sth = $dbh->prepare('Select * from table_name'); $sth->execute(); my $r; my @a; while ($r = $sth->fetchrow_hashref) { push @a, $r; } return (\@a); }

my "Improved" version would look more like this:

#------------------------# sub GetValues #------------------------# { my $dbh = get_connection('databasename'); my $ok = 1; my $msg = "Success"; my $sth; if (!$dbh) { $ok = 0; $msg = "Failed to get connection to databasename in GetValues"; } else { $sth = $dbh->prepare('Select * from table_name'); }

if ($ok && (!$sth)) { $ok = 0; $msg = "Call to prepare failed in GetValues"; }

my $r; my @a; if ($ok) { $ok = $sth->execute(); if (!$ok) { $msg = "Call to excecute failed in GetValues"; } else { my $r; my @a; while ($r = $sth->fetchrow_hashref) { push @a, $r; } } return ($ok, $msg, \@a); } #------------------------#

Note that my new code is literally TWICE as long as the old code. Often, it's longer.

Is this really advantageous? In the long run, it feels like I'm trying the same things over and over again. I actually made a module to abstract out the if-ish ness of the this routine, but the functions got to the point where they were taking 6+ paramters, some of which were optional. It was just ugly. So I've stopped using it until I can do it over again with anonymous hashrefs and "named params."

The net result is that plenty of folks are saying "error checking is over-rated", and I can see where they are coming from. What's the "right" answer?


why not just set RaiseError?

derby on 2003-09-17T15:36:16

If the get_connection method returns a handle
on which RaiseError, was set, just wrap in
an eval:

sub GetValues {
      my $a;

      eval {
              my $dbh = get_connection('databasename');
              my $sth = $dbh->prepare('Select * from table_name');
              $sth->execute();
              my $r;
              while ($r = $sth->fetchrow_hashref) {
                    push @$a, $r;
              }
      };

      print STDERR $@ if $@;
      return $@ ? undef : $a;
}

bail out on error

jmm on 2003-09-17T16:03:59

A lot of the extra code space is the constant testing whether you need to skip the next step because you've already hit an error. Just bail out when the error happens. I also changed it to either return 0, $msg (when an error happened) or else to return 1,@list (when everything worked).
#------------------------#
sub GetValues
#------------------------#
{
    my $dbh = get_connection('databasename')
      or return( 0, "Failed to get connection to databasename in GetValues" );
    my $sth = $dbh->prepare('Select * from table_name')
      or return( 0, "Call to prepare failed in GetValues" );
    my @a = $sth->execute()
      or return( 0, "Call to excecute failed in GetValues" );
    my $r;
    my @a;
    push @a, $r
        while $r = $sth->fetchrow_hashref;
    return (1, \@a);
}
#------------------------#

My style -- is this wrong?

Sifmole on 2003-09-17T16:04:04

I don't understand why people keep all kinds of status flags and hold off until the end of a routine to "return". If you know that you will do nothing after a failure at:

      if (!$dbh) {
                $ok = 0;
                $msg = "Failed to get connection to databasename in GetValues";
        } else {
                $sth = $dbh->prepare('Select * from table_name');
        }

Why not just return instead of checking $ok over and over again later? I would do...

      if (!$dbh) {
                return (0, "Failed to get connection to databasename in GetValues", \@a);
        }

$sth = $dbh->prepare('Select * from table_name');

ick

gav on 2003-09-17T17:00:08

I find code like this pretty cringe-worthy. If you set RaiseError and HandleError, you can have something this simple:

sub get_values {
    my $dbh = get_connection('databasename');
    return $dbh->selectall_arrayref(
        'SELECT * FROM table_name', { Slice => {} }
    );
}

Isn't that much nicer than 25 lines of code? I'm also not keen on returning flags for errors, you'll just have to write lots of code to check, just die if something bad happened and trap it with eval.

Re:ick

htoug on 2003-09-17T19:46:34

Yes, and most of the time, the correct reaction to something bad happening is to die, so you can even drop the eval.

{ RaiseError = 1 }

tobert on 2003-09-26T14:39:49

The best way I've found is to use RaiseError => 1, which tells DBI to always throw an exception when there is an error. This way, you never have errors go unnoticed. If you want an error to not be fatal, use eval.

eval { $dbh->do("DROP TABLE users"); }; if ( $@ ) { my_error_handler( $@ ) }

Another method I use is a function that does a little more digging for me before printing it's error message.

sub check_dbi_errors ($) { my $sth = shift; return if (!$sth->err); warn $sth->errstr; if ( $debug ) { print "SQL for statement was: \"", $sth->{Statement}, "\"\n"; ... } } my $sth = $dbh->prepare(execute( $value_for_d ); check_dbi_errors( $sth ); ...

Re:{ RaiseError = 1 }

tobert on 2003-09-26T14:56:30

Hmm, I didn't look and see that the pre tag isn't allowed. bummer.

The best way I've found is to use RaiseError => 1, which tells DBI to always throw an exception when there is an error. This way, you never have errors go unnoticed. If you want an error to not be fatal, use eval.

eval {
      $dbh->do("DROP TABLE users");
};
if ( $@ ) { my_error_handler( $@ ) }

Another method I use is a function that does a little more digging for me before printing it's error message.

sub check_dbi_errors ($) {
      my $sth = shift;
      return if (!$sth->err);
      warn $sth->errstr;
      if ( $debug ) {
            print "SQL for statement was: \"",
                    $sth->{Statement}, "\"\n"; ...
      }
}
my $sth = $dbh->prepare(
execute( $value_for_d );
check_dbi_errors( $sth ); ...