Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

Re^2: Best Practices for Exception Handling

by adrianh (Chancellor)
on Jan 29, 2003 at 17:21 UTC ( [id://231022]=note: print w/replies, xml ) Need Help??


in reply to Re: Best Practices for Exception Handling
in thread Best Practices for Exception Handling

Some reasons I like exceptions:

  1. Robustness. I can forget to check for an returned error value. I cannot forget to check for an exception.

  2. Brevity. I prefer:

    $o->foo->bar->fribble->ni

    to

    $o->foo or return(ERROR_FOO); $o->bar or return(ERROR_BAR); $o->fribble or return(ERROR_FRIBBLE); $o->ni or return(ERROR_NI);
  3. Clarity. With exception based code the "normal" flow of control is more explicit because it is not obscured by error handling code. I think that the first of the two code examples above shows the intent of the code more directly than the second does.

  4. Separation of concerns. The error condition and the error handler are different ideas.

    • You may want an error to be handled in different ways depending on the context.
    • You may also not know how the error should be handled at the point it occurs.
    • You may not know how the error should be handled at the time you write the code.

    With the return-error-code style you end up having to either:

    • propogate error conditions to where the decision on how they should be handled can be made.
    • propogating error handlers down to where the errors may occur

    Both options rapidly become messy if there are many levels of code between the error condition and the error handler.

  5. No confusion between return values and error conditions.

There are probably some more ;-)

Replies are listed 'Best First'.
Re: Best Practices for Exception Handling
by jonadab (Parson) on Jan 30, 2003 at 04:21 UTC
    > Robustness. I can forget to check for an returned error value.
    > I cannot forget to check for an exception.
    > Brevity. I prefer: $o->foo->bar->fribble->ni
    > to $o->foo or return(ERROR_FOO); [snip]

    Huh? Error return codes? Error return codes are basically an inelegant way of doing exactly the same thing -- punting the problem to some other code someplace else. It's that whole approach that I don't understand the value of. Points one, two, and five all seem to be arguing against return codes, which are in my view basically a certain (particularly inelegant) type of exception handling. You don't need to convince me that's a bad way to do it; my question is why it (punting an error to the caller's caller's caller) should ever be done at all.

    Regarding your third point, clarity: one of us is smoking crack, because putting code that handles an error pages away from where the error actually happens is my idea of severe obfuscation. How it could ever conceivably enhance clarity is entirely beyond my ability to fathom.

    Point four, however, I'd like to explore further. Maybe I just haven't encountered the right problem yet. Every error I've had to handle either could be fixed, or it couldn't. If it couldn't be fixed, it either could be logged and ignored and the program proceed less certain functionality, or else it was fatal. That basically leaves three options when an error condition pops up: do stuff to fix the problem, log it and go on, or spit an error message and exit. I have yet to encounter a situation where the caller might be relevant to the question of which of these conditions applies.

    So, back to your point four: can you suggest a real-world example or two of a situation where the error needs to be handled differently depending on where the routine is called or other circumstances not known to the code that finds the error? I've been through this discussion before (not here) and have tried to imagine such a situation, but I can't seem to come up with one.

     --jonadab

      The fact is that when you write code in modular fashion one part of your system cannot always know how to handle errors itself. In such cases the only thing you can do is pass error somewhere else and there are in general two ways to do it: exceptions and return codes. And exceptions is just a more robust way to do it.

      Example: say you are implementing business logic for your application which has multiple frontends (CLI, web and GUI). This part of your application encounters an error (let say a database connection error). What should it do? Print HTML page with error? Produce plain text formated error message for CLI? Write something in the log? No, it is not responsiblity of this part of your system to do these things, it is responsiblity of the frontend part to handle this error. So you just raise an exception and let the frontend to handle it.

      --
      Ilya Martynov, ilya@iponweb.net
      CTO IPonWEB (UK) Ltd
      Quality Perl Programming and Unix Support UK managed @ offshore prices - http://www.iponweb.net
      Personal website - http://martynov.org

        Example: say you are implementing business logic for your application which has multiple frontends (CLI, web and GUI). This part of your application encounters an error (let say a database connection error). What should it do? Print HTML page with error? Produce plain text formated error message for CLI?

        You said this was modular code, right? Right?

        Doesn't sound like it to me. If it was, you'd just call the routine that produces an error message, and let that routine use its Big Switch Statement to decide how exactly to do that. (If the routine that prints an error message encounters an error while doing so, you'd probably consider that fatal and die.)

        This sounds pretty similar to the exception-throwing way of doing things if all you're doing is printing an error message, but the more common case would be where you have to actually do something, such as ask the user for better input. In that case, you're calling a routine that asks the user for input, and it's using a switch statement to decide which routine to call -- the one that puts up a dialog box, the one that prints the question on STDOUT and gets the answer on STDIN, the one that prints up a web form and waits for the form processing script to signal it with an answer (identifying this instance by matching a hidden field token in the form against a list of such tokens and corresponding PSIDs), or the one that posts the question to usenet and checks periodically for a response. Whichever routine you call, it will have its own ideas about how to make as many attempts as necessary until it gets valid data, at which point it will return that answer to the caller.

        I'm starting to think maybe we're doing the same thing in opposite ways. You're reducing complexity by moving all knowledge of what's going on into the caller, and I'm reducing complexity by leaving the caller with knowledge only of what it wants to do with the data and moving all knowlege of where the data comes from into the subroutine. It may be a different paradigm.

        In some ways, your approach reminds me very much of my brief experiments with the event-oriented paradigm when I took courses in two "fifth-generation" languages in college. As you probably know, event-orientation turns everything around by making user input the caller and the program's logic the callee. I have a bad taste in my mouth for this approach, possibly because the only languages I've used that do things that way are VB and Lingo, both of which I loathe, especially Lingo. Though now that I think of it, it might be possible to do something like it in Perl, sort of, and that might not be so bad. [ponders this]

        I'm not sure how to fall off the end of the main code block (which presumably would initialise the objects and stuff) while leaving the various objects in place, however. It might be necessary to use some kind of big loop (which I suppose is probably what VB and Lingo do under the hood anyway). Come to think of it, the Inform standard library works something like that; I never thought of it as event-oriented, but now that I think about it, it really is. I'm sure I could do something like that in Perl (though I could never write anything as unbelievably complex as the Inform standard library; I'm convinced Graham Nelson is a genius).

        Am I completely out in left field here, or am I beginning to understand?

         --jonadab

      Points one, two, and five all seem to be arguing against return codes, which are in my view basically a certain (particularly inelegant) type of exception handling. You don't need to convince me that's a bad way to do it; my question is why it (punting an error to the caller's caller's caller) should ever be done at all.

      Point (1), (2), and (5) still apply if you're only dealing with an error code at the caller level.

      Point (1): Robustness. Consider:

      my $output = ''; my $buffer; while (my $n = read(INPUT, $buffer, 1024)) { $output .= $buffer };

      This code is broken because it is not checking for possible read errors (when read returns undef). This can lead to $output being silently truncated. If read threw an exception I could not accidentally ignore the error.

      Point (2): Brevity. If we ignore passing the error up and just die I still have much shorter code when I do:

      sub foobarfribbleni { eval {$o->foo->bar->fribble->ni}; die "failed to foobarfribbleni" if $@; };

      Than if I had to do:

      sub foobarfribbleni { $o->foo or die "failed to foobarfribbleni"; $o->bar or die "failed to foobarfribbleni" $o->fribble or die "failed to foobarfribbleni" $o->ni or die "failed to foobarfribbleni" }

      Point (5): No confusion between return values and error conditions. The problem with the code using read above is because the developer has treated an possible error value as a legal return value. With exceptions that mistake cannot occur.

      Regarding your third point, clarity: one of us is smoking crack, because putting code that handles an error pages away from where the error actually happens is my idea of severe obfuscation. How it could ever conceivably enhance clarity is entirely beyond my ability to fathom.

      The argument is that the code will work one way 99% of the time. The 1% of error conditions are, well, exceptional :-)

      With exceptions you can show the way your code works 99% of the time, without cluttering it up with the error handling code. The foobarfribbleni subroutines given above are one example. Another would be DBI, which you can switch between throwing exceptions or returning error codes. Compare:

      # With exceptions eval { $sth = $dbh->prepare(q{ SELECT region, sales FROM sales_by_region +}); $sth->execute; my ($region, $sales); $rv = $sth->bind_columns(\$region, \$sales); while ($sth->fetch) { print "$region: $sales\n"; } }; die $dbh->errstr if $@;

      with

      # Without exceptions $sth = $dbh->prepare(q{ SELECT region, sales FROM sales_by_region }) or die $dbh->errstr; $sth->execute or die $dbh->errstr;; my ($region, $sales); $rv = $sth->bind_columns(\$region, \$sales); while ($sth->fetch or die $dbh->errstr) { print "$region: $sales\n"; }

      Personally, I find the exception throwing version easier to parse without the sprinking of die statements.

      Point four, however, I'd like to explore further. Maybe I just haven't encountered the right problem yet. Every error I've had to handle either could be fixed, or it couldn't. If it couldn't be fixed, it either could be logged and ignored and the program proceed less certain functionality, or else it was fatal. That basically leaves three options when an error condition pops up: do stuff to fix the problem, log it and go on, or spit an error message and exit. I have yet to encounter a situation where the caller might be relevant to the question of which of these conditions applies.

      An example:

      We have a registration system App::RegisterUser that needs to store usernames and e-mail addresses in a database. The usernames and e-mail addresses need to be unique.

      App::RegisterUser forms part of a larger system App. All the DB access goes through App::DBI which has all the common DB access code in it.

      App::DBI is built on top of DBI.

      We take our data integrity seriously, so we have registered uniqueness constraints on the underlying database tables so that attempting to insert duplicate usernames or email addresses causes an error. This also allows us to get around any race conditions since inserts and updates are atomic.

      So, an attempt to add a duplicate user with App::RegisterUser will cause an error at the DBI level.

      What is the right response to the error?

      When I use App::RegisterUser as part of a web-based registration system the correct response is to tell the user that the name/email is already used and try again.

      When I use App::RegisterUser as part of a bulk registration system the correct response is to log the error for later use.

      So, we have two possible responses to the error from App::RegisterUser, which comes from App::DBI, which comes from DBI.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://231022]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others surveying the Monastery: (3)
As of 2024-03-29 06:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found