This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.

Title

Are all errors exceptions?

Description

<abstract>The following ruminations were written seven years ago but have held up remarkably well. They have been published with minor updates.</abstract> This article deals with the situation illustrated below, specifically the question raised in the comment. <pre><c>if (! $folder_id) { $this->db->logged_query ("SELECT folder_id FROM" . $this->app->table_names->objects . "WHERE id = $obj->object_id"); if ($this->db->next_record ()) $folder_id = $this->db->f ("folder_id"); else // raise exception? ignore? what to do? }</c></pre> Above we see a situation in which you may decide against a stricter enforcement because whereas the error is clear, the reaction is not. This is also a big part of working with contracts: deferring reactions. Often---especially when developing libraries---you're in code so deep that the desired reaction could be one of many depending on the deployment of that code. The code above is taken from the publishing loop in the <a href="{root}/software/webcore">webcore</a>; it's used to publish comments. In effect, the code has detected that a comment object id has been passed in that doesn't correspond to anything in the system. It's bogus. It's wrong. Some deployments---I would hazard most---would just like to silently ignore the error and publish as much as possible. Silently ignoring an error will always bite you in the ass in the end (pun intended). The key here is that whereas the person deploying the final system should be perfectly free to ignore the error, you, as the library developer, can and must not. Let's see what kind of reactions we could have here. Well, isn't that what exceptions were invented for? They're for transmitting error conditions up out of deep library code. Problem solved. For more severe errors in which the code cannot continue, the answer is quite clear: you simply throw an exception. However, in the situation above, it's not so clear. The problem is easily skipped and most of the rest of the job can be finished. Here is where deferral comes in. Just call a function that will handle it later. This function can log the error or warning, display it to the user, ask to abort/retry/ignore, consult a table for same, throw an exception or just ignore it. It's not your problem to dispatch solutions to encountered errors. It's your job to detect them and maintain the integrity of the running code. Simply throwing an exception no matter what the error condition is, in effect, making a decision about how the error will be handled. Control is lost because the exception handler is necessarily higher up. This is a bad thing if you'd actually like the code to do the best it can. As any experience at all will have shown you, some errors are just warnings or hints. It's not just black and white, error or not. Many deployments of the system containing the code above will actually treat the issue as a warning and log it for the database techs to address. However, to assume the opposite, that callers want errors to be swallowed, cheats those callers as well. It cheats them because it becomes incredibly hard to find errors; they must be detected by subtle logic or data problems (e.g. Hmmm...the log shows it only sent 500 emails, I thought there were 503 subscribers...). If the system never complains or logs anything, the end user calls you first. It cheats you because you can never adequately test your system because it never complains. Everything's OK. It kind of works. It <i>mostly</i> works. The desire for safety or avoiding crashes or exceptions on the client side should never override the desire to have correct code that detects error conditions. If you write library code or end-user code, that code deals mostly with detecting and reporting misused functions. The functionality itself is generally straightforward; it's wrapping the interface around it that's hard. The only thing that you'll probably spend more time on is hunting down memory bugs---and, yes, even if you're using a garbage-collected runtime, you can still have memory bugs. What would you call it if the memory required by your publication script increases in proportion to the number of subscribers and mails? If you're wondering what I ended up doing in the case above, I decided on a function call 'raise'. It sounds like an exception, and that's usually what happens: it breaks the code on that line, but a little more elegantly than the usual PHP <c>die</c> statement. While the default handler for exceptions simply issues a fancy <c>die</c> statement, that handler can be replaced with a different one, one that redirects to an HTML page with a nicely formatted error printout and a form for submitting the error. Since this code is likely to run inside a script that just wants to send subscriptions and doesn't care about data integrity errors, the handler would probably be replaced with something that suppresses the exception, but logs the error. That way, once the subscription run is done, you can view the error log and see if there are data integrity problems, and, perhaps more importantly, you see them all at once instead of just one at a time. And, more importantly still, those subscribers for whom there were no problems received their mail on time.