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
Re^2: Best Practices for Exception Handling
by adrianh (Chancellor) on Jan 29, 2003 at 17:21 UTC
|
Some reasons I like exceptions:
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);
$o->bar or return(ERROR_BAR);
$o->fribble or return(ERROR_FRIBBLE);
$o->ni or return(ERROR_NI);
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.
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.
No confusion between return values and error conditions.
There are probably some more ;-)
| [reply] [d/l] [select] |
|
> 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
| [reply] [d/l] |
|
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
| [reply] |
|
|
|
|
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.
| [reply] [d/l] [select] |
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
| [reply] |
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)
| [reply] |
|
> 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
| [reply] [d/l] |
|
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) | [reply] |
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.
| [reply] |
|
|