http://qs321.pair.com?node_id=230975


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

I tend to agree. A lot of people with a lot more experience than I have rave about how wonderful exception handling is, but they've failed to communicate to me why it is valuable to be able to throw an exception in one place and catch it someplace else rather than handling the problem (assuming it can be handled by any means other than spitting an error message and bailing) in the same block of code where it is detected. It seems to me that having the exception and the handling thereof separated by arbitrarily many lines of code is asking for headaches in terms of code maintenance. Rather than throwing the exception in the first place, wouldn't it be better to handle it en situ? (Sure, sometimes a number of spots can share the same code -- so call a subroutine then, but at least the call is right there.)

As I said, I have a good deal less experience than a lot of the people who rave about exception handling, so I'm probably missing something...

 --jonadab

Replies are listed 'Best First'.
Re^2: Best Practices for Exception Handling
by adrianh (Chancellor) on Jan 29, 2003 at 17:21 UTC

    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 ;-)

      > 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

        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.

Re: Re: Best Practices for Exception Handling
by v_thunder (Scribe) on Jan 29, 2003 at 16:00 UTC

    I believe that the idea is that you can't always do something on the spot.

    For example, suppose I am developing a flashcard application that reads flashcards from a file and displays them in a pretty window. To accomplish this, I decide to write a couple of classes that do the actual file reading and parsing, etc., in order to keep that separate from the gui code.

    So what happens if, for example, the gui asks one of those objects to open a non-existant file? An elegant way out of the problem is for the object to raise an exception. The gui can then choose to catch that, and pop up a dialog, or whatever.

    It is definitely not the *only* way to solve the problem, though. You could also, for example, have every method return error codes. Or a hash containing an error code plus whatever other value it wants to return. Exceptions do provide a very clean way to do it, though.

    Just my $.02,
    -Dan

Re: Re: Best Practices for Exception Handling
by Ovid (Cardinal) on Jan 29, 2003 at 17:21 UTC

    Java Boutique has an interesting article about exceptions that you might find useful.

    What I like about exceptions is fairly straightforward. If I have a GUI (A) which issues a message to some object (B) which in turn sends a message requesting data from object (C), what happens if C determines that the data is bad due to user input (and not, say, a programming error)? There's probably no point in issuing a warning in the error log (because it's not an error from a programming standpoint) and there's no point in killing the application because a user typed something stupid (an all too common event, though).

    In the above scenario, C throws in exception and B catches it. B then checks the exception to see if anything unusual needs to be done. If the exception is caused by failing to select a required option, no big deal. If the exception is caused by a bad password, perhaps it is a big deal. B can decide whether or not to log the exception and do further processing. B, at this point, rethrows the exception and A catches it. Because A is simply the GUI, A doesn't worry about logging the exception. A simply has to have some method of displaying the exception to the end user in a meaningful way.

    Thus, with an exception based system, particularly if it's multi-tiered, you have full control of the error processing rather than simple die and warns. Further, uncaught exceptions will kill the program when encountered during execution, thus making them morely likely to be reported in testing and feedback -- which in turns means they are more likely to be handled appropriately and leads to more robust code.

    Cheers,
    Ovid

    New address of my CGI Course.
    Silence is Evil (feel free to copy and distribute widely - note copyright text)

      > If I have a GUI (A) which issues a message to some object (B) which
      > in turn sends a message requesting data from object (C), what
      > happens if C determines that the data is bad due to user input

      See, that's where I always get lost. If C is getting user input, and the user input is invalid, C should just reget the user input. When it has valid input, it should pass that back to B. Otherwise, it should continue to ask the user to fix up the input.

      In a slightly different scenerio, C might think the input is okay (there's an integer in this integer field, and some text in this required text field; C does not know the meaning of these data, but they are the kind of data that were requested), but B might discover that it's inconsistent. (There is no thirty-first of February.) In that case, B would possibly explain the problem to the user and then call C again; only when the input is good enough for B to do its job does it then pass the result back to A.

      Do you see where I'm getting hung up? It's not with the question of HOW to pass stuff back to the parent (which was the original topic of the thread, I know), but more with the more basic question of why that is a thing that needs to be done. That's the point I haven't managed to understand yet.

       --jonadab

        jonadab wrote:

        If C is getting user input, and the user input is invalid, C should just reget the user input. When it has valid input, it should pass that back to B. Otherwise, it should continue to ask the user to fix up the input.

        I see your point, but I look at it differently. You want to decouple your functions. Your function that validates the data should do just that: validate the the data. Either the data is validated or it throws an exception which other portions of your program might handle differently. This function's role is not to acquire the data. If you do that, you have a less robust function as you're forcing it to do too much. You could conceivably get around this by passing the function an object that knows how to fetch data, based upon what type of object it is, but I still feel this is doing too much. Consider the following:

        
          +--------------+
          | presentation |
          +--------------+
                 |
          +--------------+
          |   dispatch   |
          +--------------+
                 |
          +----------------+
          | business rules |
          +----------------+
                 |
          +--------------+
          |    db api    |
          +--------------+
                 |
          +--------------+
          |   database   |
          +--------------+

        That might be the tiers of a multi-tiered system that allows each tier to be developed seperately and replaced, if necessary. Your data validation might be in the "business logic" tier or in your "database API" tier, depending upon your needs. How does something in one of those two tiers refetch the data? They then have to know about the dispatch and presentation layers -- that's not good. As much as possible, different layers should not be overly dependant on one another or changing something in one layer will affect the others and drive up maintenance costs.

        Instead, let's say that the validation (and I know this is not a great example) is in the database API layer. It does not know, nor does it care, why the validation fails. It just throws an exception. That exception is caught by the business logic layer and it determines if this is user error (they forgot to select from a menu), or something more serious (the third failed login attempt). At this point, the business logic layer can decide whether or not to log the error, rethrow the error, or do something entirely different. The dispatch layer might catch an error, if thrown, and send an error object to the presentation layer. The presentation layer is not going to want to handle the error the same way. It might simply say "bad username/password combination", while the business logic layer wants to know if someone is brute-forcing a login.

        In other words, exceptions allow you to decouple different portions of an application and allow each section to determine how to handle the exception. How this is handled will vary depending upon the layer, but it can help to ensure more robust applications by providing a mechanism by which we are more likely to spot problems and handle them where they should be handled.

        Hope that helps :)

        Cheers,
        Ovid

        New address of my CGI Course.
        Silence is Evil (feel free to copy and distribute widely - note copyright text)

Re: Re: Best Practices for Exception Handling
by hding (Chaplain) on Jan 29, 2003 at 17:14 UTC

    Handling the error in place and consulting the calling code for advice on handling it are not necessarily mutually exclusive, for which I point you to the paper referenced in another node of mine in this discussion which (i.e. the paper) describes how things are handled in Common Lisp.