|Think about Loose Coupling|
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:
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:
Than if I had to do:
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:
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.
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.