PHP5 Exception Use Guidelines

Authors:

This is an informal document to describe usage guidelines for exceptions in PHP5.

This document is targeted for package developers, although it will be relevant for anyone integrating PHP5 PEAR packages into their applications.

Please feel free to add comments or suggests directly to the document, but use blockquotes and markers unless you are just fixing typos or grammar, etc. For example:

I think this section should say something about feature "blah".
--Joe
Ok, well, what specifically should it say?
--Fred

Overview

This document is an RFC, aiming to specify error handling in PHP5. The upcoming release of PHP5, introducing Exception handling mechanisms, prompted PEAR to revise coding guidelines in respect to error handling.

It is important to refer that this document does not address handling of warnings or notices. It is felt that a solution for warning treatment may be different from error handling. Inclusion of warning processing could introduce too much noise into the discussion of the error handling issue and is therefore
postponed.

Target population

This document should be read, criticized and participated by all voting members of PEAR, in the process of building up the PHP5 coding guidelines.

Document structure

The document is divided into three large sections. The first is an introduction to Exceptions, focusing on the differences between Exceptions and the current error handling mechanism (return error objects). The second section is an analysis of the relative merits of Exceptions, and potential pitfalls on the usage of Exceptions. The third section composes the coding guidelines themselves, which upon approval should be made part of the coding guidelines document for PEAR on PHP5.

Introduction to Exceptions

Exceptions provide one solution to error handling in programming languages. Exception handling is the major alternative to the usage of special error return codes to signal a function call failure.

Basic Functionality

The basic exception handling structure looks like this:


<?php


try {
   // Normal execution code lines
   // Normal execution code lines
   // Normal execution code lines
} catch (SomeException e) {
   // Exception handling code lines
   // Exception handling code lines
} catch (SomeOtherException e) {
   // Exception handling code lines
   // Exception handling code lines
} 
?>

The structure must have a try block, and one or more catch blocks. Each catch block specifies which exception class is handled by the catch block.

If normal execution code produces no errors, only the code inside the try block is executed. and program execution skips all the catch blocks, proceeding with the first instruction below the exception handling structure.

If one instruction in the normal execution code, inside the try block, throws an exception (causes an error), execution of the try block is halted. No more lines of the try block will be executed, and exception handling takes place.

Exception handling works by travelling up the call stack, looking for a matching catch block: For each level of the call stack, exception handling code will check exception handling structures that surround the current execution point. A catch block matches if it specifies the class of the current exception, or one of its parents.

Once a matching catch block is found, its code is executed. Program execution will resume after the matched exception handling structure.

Exception handling inside a given method is pretty straightforward to visualize. You travel outward from all the nested try blocks, looking in the appended catch blocks for a matching one (one where the current exception fits in).

Exceptions Bubble Up

The "magic" happens once an exception isn't matched inside a method. It is then assumed that the method can't handle the exception, and exception handling continues, by moving one step up the call stack - to the caller method, if necessary repeating the process until finally a matching catch() is found or the top-level is reached. This allows for exceptions to bubble up through middle software layers, and get caught on top layers, which may have enough information to try an alternative.

If no layer catches an exception, an uncaught exception condition occurs. Program execution is halted, and usually exception data is shown to the user. Uncaught exceptions should be considered bugs.


<?php

class File {  
  ...
  function copy($to) {
    if (!copy($this->path, $to)) {
    throw new IOException("Could not copy file!");
    }
  }
  ...
}

class Project {
  ...
  function setup(File $template) {    
    $template->copy($this->projectPath);
    $this->initialize();
  }
  ...
}

class CreateProjectAction {
  ...
  function main(Request $r) {   
    try {
      $tpl = new Template($r->getParam('type'));
      $p = new Project();
      $p->setup($tpl);
    } catch (Exception $e) {
      $this->log($e);
    }
  }
  ...
}

?>

In the example above any errors that happen in the low-level file copy will pass through the middle Project::setup() layer and be caught by the catch() {} block in the CreateProjectAction?::main() method.

Exception Taxonomy and Rich Context

By nature of being objects derived from a base Exception class, exceptions allow developers to classify errors using class hierarchies. Additionally, exception subclasses can store additional information that is relevant to the specific type of exception.

Error taxonomy in an aplication is something that can be quite complex. While all errors can provide textual descriptions, specific errors can provide structured information useful in recovery procedures. For instance, an external command execution error can contain the command return code for further analysis.

Exceptions, by using classes to represent errors can represent the error taxonomy quite accurately. Exceptions allow a developer to describe errors much like any other entity in the application. Starting from the top, the generic Exception provides much the same facilities that regular 'return code errors' provide. However, when specializing exception classes, they can be extended to include data specific to the error condition causing the exception.

This rich taxonomy is used while catching exceptions. Catch blocks selectively handle only the exceptions they know how to recover from. As exceptions are arranged hierarchically, it's trivial for a catch block to handle groups of related exceptions, since these will inherit from a common exception class.

One interesting result of using class hierarchy to describe error taxonomy is the deprecation of error code enumeration, which becomes obsolete.

Exceptions are Catch-Oriented

Exception error handling places the emphasis on the handling reather than the throwing. This is an interesting difference between solutions like trigger_error() and even the current design behind PEAR_Error and other return-status-based error handling.

Exception-based error handling leaves the decision of how to handle errors up to the calling code. A low-level package (like PEAR libraries generally are) does not know what the severity of a failed database connection should be; it throws an Exception which is then caught by an upper layer which can decide whether to log & move on or to terminate the execution of the script. This non-assuming nature provides consistency in behavior while allowing for flexibility in interpretation.

Another important aspect of this is recovery. Not only does the calling code know how to handle the severity of an exception, but is also the code that knows how to recover from an exception (e.g. to try to load the resource via HTTP when the file loading strategy threw an exception).

Exception Wrapping

Exception wrapping is a technique used to avoid breaking layer abstraction due to exceptions. Whenever an exception is bubbling up through a middle software layer, it is wrapped inside an exception defined by the middle software layer, so upper layers don't get exposed to the internal implementation.

This is probably easier to explain with an example. Figure a three layer software: application layer (top), data-access layer (middle), file manipulation layer (bottom). The middle layer shields the top layer from data storage mechanisms. Data may be stored in a filesystem as easily as in a database.

In this scenario, if we allow FileExceptions? to bubble through the middle layer unmodified, the top layer will be exposed to the implementation of the data-access layer (wrong!). The solution to this is exception wrapping. In an error condition, the middle layer will always throw a MiddleLayerException?. However, since we don't want to lose information about the lower error -- remember, we don't know how the exception might be handled -- we include the LowerLayerException? as a data element in the thrown exception. Middle layer code would look like this:


<?php

try {
    someLowLayerCall();
} catch (LowLayerException $e) {
    throw new MiddleLayerException('Some info', $e);
}
?>

Most PEAR packages are low level libraries. They should throw exceptions when things go wrong in unexpected (and unchecked) ways. A low-level method that is designed to read data from a file would throw an exception if the file could not be opened.

Another kind of exception is an application exception which will be less common in PEAR packages due to their relative single-layered nature. An application exception would be thrown based on business rules in your application. Perhaps a call to a save() method would throw a ValidationException? if the object could not first be validated. These are higher-level exceptions that will be found in the applications that use PEAR packages.

In many cases it may make sense to wrap one exception inside another to add context to the error and to change it from a low-level to an app-level exception. For example, a low-level IOException might get wrapped inside an application-level LoadingException?. This way the low-level IO exception is given a context that is meaningful to the middle or top tiers of the application.

So, rather than:

"IOException -- WTF? Oh well, display generic error page."

We have:

"Ahh, I know what a LoadingException? means; it means display the form again and ask them to check the filename they entered."
The text above sounds reasonable, the problem is that it can very quickly degenerate into a not-so-useful Anti-Pattern, see for example "Exceptions Mask Real Problems", and the ever interesting "Exception Funnel AntiPattern"
-- Jesus M. Castagnetto

Fundamental differences to error returning

Exceptions can be seen as the result of a fundament difference in approximation to the subject of error handling. Two assumptions are made:

Error severity is dependent on the software layer

The layering of software shields layer design from application context and from implementation details. Lower layers are shielded from application context introduced by upper layers. Upper layers are shielded by implementation details of lower layers (all they see is a service, such as storage).

Layered design allows large scale software, and such beautiful things as libraries. It does complicate error handling, however. The lack of context in lower layers means that they can't possibly specify the application-wide error severity.

Exceptions reduce the burden of defining error severity. Error severity is defined in a per-layer basis, and even then, only by the behaviour of error handling. On each layer, the error is either fatal for the layer (thrown- or bubbled-up) or correctable by the layer. The error is fatal application-wide iff it is fatal for the top layer.

Non-handled errors are bugs, or error handling is mandatory

Whenever an exception is thrown or bubbles past the top layer, an uncaught exception condition occurs. Here, the behaviour is defined by the execution environment. In PHP, uncaught exceptions always halt the application. It would be perfectly possible to return to the origin of the exception and proceed from there, but language designers shun the option of silencing errors and proceeding, in fact forcing developers to handle all errors.

Note: I don't know of any exception-enabled language where it is possible to disable app halting on uncaught exception conditions. If anyone knows any, please place a reference here.
-- Sérgio Carvalho

Further Reading

Here are some additional links that provide some useful insight on how and when to use exceptions in different languages:

This article should be required reading to all using Java. Hopefully, exceptions in PHP will not evolve into the Java approach, rather than the more practical Python approach.
-- Jesus M. Castagnetto
In Python, as opposed to the Java approach, exceptions are unchecked, so you are free to do what you think is best for your code
-- Jesus M. Castagnetto

There's lots to read on the general debate over Exceptions vs. error-status codes. Here are a couple useful (point, counter-point) references:

Exceptions advantages and pitfalls

This section should summarize all points in favour and against exceptions. For each point, a pseudo-code example should be provided. Rebuttals should come at the end of the point explanation text.
--Sérgio Carvalho

Advantages

Deferral of error checking

Using exceptions, error checking does not need to happen immediatly after command execution. Commands may be grouped into logical units, whose error checking is done in one place only. Example:


<?php

try {
    $person->setName('John Doe');
    $person->setAddress('Something St. 12');
    $person->setbirthDate('10-10-1900');
    $person->store();
} catch (Exception e) {
    throw new DataPopulationException(
        'Unable to fill data for person ' . $person->getId(), e
    );
}

?>

Deferral of recovery

Exceptions, as they bubble up, allow for recovery to take place in upper layers, which have contextual information enough to try alternatives.

As an example, consider an FTP getter function with this prototype:


<?php


// Receives an URL, and returns document contents as a string. 
// May throw FtpException in case of error.
function ftpFetch($url);
?>

Suppose you base a pref class on this function, with prefs stored via FTP:


<?php

class Prefs {
    ...
    function retrieve() 
    {
        $unparsedPrefs = ftpFetch('ftp://ftp.example.com/myApp/prefs.xml');
        $this->parse($unparsedPrefs);
    }

}  

?>

Now, one layer above, there's your application, trying to retrieve prefs.

<?php

    try {
        $prefs = new Prefs();
        $pres->retrieve();
    } catch (Exception e) {
        // Oops, could not retrieve prefs. However, 
        // the app knows this is a minor error. Will proceed 
        // with default values
        $prefs = PrefsFactory::createDefaults();
    }

?>

Of all the three layers, only the top one knows the original FtpError? is a minor one. All other could not handle it locally, and exceptions provided the method for deferring treatment.

Easy correspondence to transactions

Add comment that transaction-exception matching is not automatic, to address pitfall suggesting that exceptions don't always correspond to transaction blocks.

Better error information

Built into PHP5, no need for (much) extra code

One interesting thing to consider is that oftentimes what some people really should be using is an assertion to check for sanity, in particular when something is really critical. Having a method throw an exception does not always guarantee this sanity, as one can easily use an empty catch and all hell will then break lose. PHP has had assert() for ages, but I never saw that its existence caused all of the code being written to conform to a "Design By Contract" methodology, although that will be a very very good thing IMHO.
-- Jesus M. Castagnetto
If you're talking strictly about sanity, an assertion does make some sense, but it is a development-only kind of thing. Deploying assertions in live code will just cause headaches if they fail. Also, since PHP code is intepreted, this just adds extra parsing overhead for somehting that's not used. IMHO you should be throwing an exception for anything you would assert() as it forces the developer to handle it somehow. If they don't, the program will stop, as it should. If the developer puts a try/catch that does nothing around all of the code (or the "assert-ing" code), then it's no different than using assert_options(ASSERT_ACTIVE, 0);
-- Justin Patrin

Drawbacks and Pitfalls

Exceptions are certainly different than simpler return-code-based error systems. The goto-behavior of throw is hard to follow sometimes. Also, the fact that exceptions bubble up through the stack forces developers (particularly developers with procedural development background) to think in a new dimension.

Exceptions Will Not Work With PHP4 Code

Exception handling in PHP is new with PHP5. PHP5 applications that use Exception handling cannot be run under PHP4. Also, libraries designed to work on both PHP4 and PHP5 cannot throw exceptions for the same reason.

While this may be obvious, it is worth mentioning as it does impose some limitations. In short, using exceptions restricts your library to being used on PHP5 only.

Exceptions May Not Fit Application Design

Procedural Code

Exceptions fit most naturally with OO application design where code is broken down into classes and methods that each perform a meaningful task.

If you are designing a procedural application, you may find that exceptions prove more clumsy than return-based error handling. This is probably not of primary concern for PEAR, however, which is a collection of OO libraries.

Collections of Errors

Unlike inert objects like PEAR_Error, exceptions also do not naturally lend themselves to being aggregated or collected, since executions jumps out of immediate context when an exception is thrown.

For example, exception throwing doesn't work well with dependency or other validation checking.

If the PEAR installer were to throw an exception on an unmet dependency, output may look like this:


$ pear install package1
package1 depends on foo
$ pear install foo package1
install of foo OK
package1 depends on bar
$ pear install bar package1
install of bar OK
package1 depends on annoyance
...

Obviously, we want to see:


$pear install package1
package1 depends on package foo
package1 depends on package bar
package1 depends on package annoyance
package1 optionally depends on package shippers

The solution is to not use exceptions for individual validation messages. A single composite excpetion should be thrown which contains information about all of the failed validation steps (in this case unmet dependencies):


<?php

$dependencies = $package->getDependencyTree()->toArray();
$failedDependencies = array();
foreach($dependencies as $dependency) {
    if (!$dependency->isSatisfied()) {
        $failedDependencies[] = $dependency;
    }
}
if (count($failedDependencies) != 0) {
  throw new FailedDependenciesException(
    'Dependencies not met',
    $failedDependencies
  );
}

?>

It is also debatable whether exceptions should be used for validation at all. This is a borderline case of "Exception as flow control" since a validation error is hardly an exceptional event.

Can Be Hard to Follow

The fact that exceptions immediately stop execution and jump to nearest catch block can make code execution paths hard to anticipate when looking at the sourcecode. It is especially important, therefore, to ensure that any caught exceptions are logged in detail before the application recovers (or not) from the error.

This gets back to the earlier-mentioned point that programming with exception is quite different from procedural, sequential programming.

Exceptions as Flow Control

This generally refers to the practice of using exceptions to describe behavior that is not at all exceptional, in order to facilitate some very fancy and hard to follow goto-style code jumps. Certainly it's important to recognize that any error handling is using errors to control the execution flow of your program; that in itself is not wrong, but the raison d'etre for error handling systems.

Note also that the more unbridled-type of exception-as-flow-control happens with some frequency in languages like Java and (apparently) Python. It does make code harder to read, though, and consensus for PHP seems to hold that this is poor design. -- Like using 'break' or 'continue' statements is poor design: i.e. people will certianly do it anyway. Thankfully this (ab)use of exceptions is more typical of application exceptions and so shouldn't be a big issue for PEAR packges.

A good rule of thumb is that if you are using an exception to achieve the effects of a goto statement without actually flagging an error, then you are using exceptions for flow control. For example (based on example from c2.com wiki), consider a recursive function that performs a tree search:


<?php

/**
 * Recursively search a tree for string.
 * @throws ResultException
 */
public function search(TreeNode $node, $data)  {
    if ($node->data === $data) {
         throw new ResultException( $node );
    } else {
         search( $node->leftChild, $data );
         search( $node->rightChild, $data );
    }
}

?>

In that example the ResultException? is simply using the "eject!" qualities of exception handling to jump out of deeply nested recursion. When actually used to signify an error this is a very powerful feature, but in the example above this is simply lazy development.

Lack of flexibility for Error Handling Schemes

Add note here about how single severity level is both an advantage and pitfall.

If we switch to only Exceptions for error handling, developers must use them even if they would rather use return values. Will some kinds of errors be allowed to use return value or will we require an Exception? Example: a login fails, should the error be an Exception?
-- Justin Patrin
I would say that the example of login failing would not be an exception. In that case the authenticate() method really should be returning TRUE or FALSE to indicate whether the credentials matched. A failed login is not an exception, because you really expect it half the time. If the authenticate() method could not open the database needed to check to see if user could log in then an exception should be thrown. In terms of the bigger issue of whether return codes can be used at all. I would say that return codes can definitely be used, but I would consider this less error handling than "status reporting". For example, perhaps that authenticate() function could return a few different statuses: LOGIN_FAILED_BAD_PASS, LOGIN_FAILED_NO_SUCH_USER, etc. To me that seems legitimate, but it also seems less like error handling.
-- Hans
Then should DB::connect return false or throw an exception when it can't connect to the DB? I realize that this function is supposed to return a DB object and that is a good reason for throwing an exception instead, but if you overlook that, this is the same case as a login method. Login fails, DB connect fails. Both are possibly expected outcomes of these methods, both are errors, and both (usually) mean that the application can't continue normally. IMHO, these are the same thing and should be treated the same. If we choose Exceptions, all errors, regardless of expectation, should be thrown as exceptions. If a developer expects an error, they should catch it and handle it. If you want to find out if a person can login with a return value, you should use a canLogin() method. Login is supposed to log you in, so a failed login is an error and therefore an exception.
-Justin Patrin
Good question. Of course at some level your code has to decide that something is an exception vs. just a notice (or warning). So there is a basic severity determination made. You are right that things should be consistent, but there's also room for grey area. I would probably make an authenticate() method return TRUE/FALSE and then have a getReason() method if false. You could also argue for throwing an exception from that method, but I see that as a not particularly exceptional event. I think, however, that mixing returning PEAR_Exception objects with throwing PEAR_Exception objects would only lead to confusion. You can certainly return some sort of status code, but this shouldn't be confused with an error code. Does that make sense?
-- Hans

Error recovery silences recoverable errors, causing unexpected results from the end user point of view

Errors that are fatal for a low-level package may be only minor annoyances to a mid-level or high-level package, but still remain an error condition. For instance, the inability to set preferences in the example for section "Deferral of Recovery" is not an error - the caught low-level exception allows complete error recovery. However, if user ABC was trying to set preferences for a mailbox application by uploading a configuration file, and there is a problem with the file caused by a bug in the saving process, the preferences will simply load with defaults, and no error will be reported. Tracking down this bug will be exceedingly difficult unless the mid-level package re-throws the exception as a warning to the user - but does not terminate the flow control. In other words, it IS possible to raise errors and leave them unhandled, even with exceptions - they do not make this problem less common, but can actually make it more difficult to track down the source of a problem. Fortunately, PEAR_Exception alleviates a large portion of this risk by allowing observers to attach to every exception's creation - a hugely important debugging aid NOT found in the built-in PHP5 Exception class.

The default preferences work just fine - there is no error condition - but they might be the wrong preferences, a decision that the program cannot make for the user (Microsoft syndrome - avoid any warnings to the user, and instead make all decisions for them).

I don't see this behaviour as an inherent failure of exceptions. It's the result of poor error handling, and could as easily be achieved with PEAR_Error.
This probably merits an entry in the coding guidelines. Something along the lines of: "Incomplete error recovery must be notified using PEAR_Warning (or whatever solution PEAR finds for handling warnings)".
-- Sérgio Carvalho
I think strictly speaking it is a good idea to mention this as a pitfall of exceptions. I don't think it is a pitfall of exceptions vs. PEAR_Error, however, as it's just as easy (or easier?) to silence errors in that system. Perhaps someone can factor these comments into the text (and remove them)? If not, I can also do it late. (Just don't want to throw away opinions.)
-- Hans

Safe to remove this section? It's really nothing to do with exceptions. --Hans

Native PHP5 exceptions don't support observers or escalating severity

In summary, using only the throw statement to *raise* errors is insufficient for some common error conditions. The Exception class can be easily extended to handle some of these conditions. A wonderful solution (under 150 lines) is to extend the Exception or PEAR_Exception class to allow nesting multiple error conditions within one object, and in addition, attaching an error severity similar to the error_reporting() used by trigger_error().

This would also allow re-packaging of less severe errors from a lower level package into more severe errors - something the simple throw mechanism does not standardize.

Attaching an error severity to an exception is contrary to the exception design fundamentals. I've added an entry in the subsection listing the fundamental difference between exceptions and error codes, explaining this in detail. Exceptions are either fatal or non-fatal, and this severity is a result of handling, not a native property. They're non-fatal if recovered, fatal otherwise.
Severity is more appropriate to the concept of warning. See my comment in the point about silencing of recoverable errors.
-- Sérgio Carvalho

<?php

// PEAR_Exception's addObserver is used to throw warnings
PEAR_Exception::addObserver('catchWarning');
function catchWarning($e)
{
    if ($e instanceof My_Warning) {
        throw new App_Exception_CantDoThat(...);
    }
}

?>

However, this functionality is not already built into PHP5's exception handling, and must be standardized in PEAR PHP5 packages prior to deployment, or there will be a huge mess.

The concept of observer is much more appropriate to warnings than it is to exceptions. Exceptions, by design, are notified to every interested observer (catch blocks along the call stack). Warnings, as they don't break execution flow, really need observers to be useful. I'd place the concept of observer, along with the concept of warning level, in a separate warning mechanism, independent from Exceptions. This document postpones discussion of a warning mechanism to another RFC, since warnings are a whole different beast.
-- Sérgio Carvalho
Again here, I agree w/ Sergio that this stuff belongs more with warnings, but probably it should be listed as a pitfall. Let's just get the comments incorporated. Perhaps need some rewording in escalating severity section. While not supported natively, escalating severity can be implemented based on convention (e.g. using commonly derived classes for warnings so that they can be thrown()) and is not a limitation of exceptions.
-- Hans

Safe to remove this section? It seems to be already dealt with in the "major differences" section, as Sergio mentions. PEAR_Exception does support observers, so that is a non-issue. --Hans

Coding Guidelines

Summary

This document defines guidelines for error handling within PEAR, for PHP5 packages. It was written to cope with Exceptions, introduced in Zend Engine 2 as the error handling mechanism.

Definition of error

An error is defined as an unexpected, invalid program state from which it is impossible to recover. For the sake of definition, recovery scope is defined as the method scope. Incomplete recovery is considered a recovery.

One pretty straightforward example for an error:


<?php

/*
 * Connect to Specified Database
 *
 * @throws Example_Datasource_Exception when it can't connect
 * to specified DSN.
 */
function connectDB($dsn) {
    $this->db =& DB::connect($dsn);
    if (DB::isError($this->db)) {
        throw new Example_Datasource_Exception(
            "Unable to connect to $dsn:" . $this->db->getMessage()
        );
    }
}

?>

In this example the objective of the method is to connect to the given DSN. Since it can't do anything but ask PEAR DB to do it, whenever DB returns an error, the only option is to bail out and launch the exception.

The next example will introduce the concept of recovery:


<?php

/*
 * Connect to one of the possible databases
 *
 * @throws Example_Datasource_Exception when it can't connect to
 * any of the configured databases.
 * 
 * @throws Example_Config_Exception when it can't find databases
 * in the configuration.
 */
 
function connect(Config $conf) {
    $dsns =& $conf->searchPath(array('config', 'db'));
    if ($dsns === FALSE) throw new Example_Config_Exception(
        'Unable to find config/db section in configuration.'
    );
    
    $dsns =& $dsns->toArray();
    
    foreach($dsns as $dsn) {
        try {
            $this->connectDB($dsn);
            return;
        } catch (Example_Datasource_Exception e) {
            // Some warning/logging code recording the failure
            // to connect to one of the databases
        }
    }
    throw new Example_Datasource_Exception(
        'Unable to connect to any of the configured databases'
    );
}

?>

This second example shows an exception being caught and recovered from. Altough the lower level connectDB method is unable to do anything but throw an error when one database connection fails, the upper level connect method knows the object can go by with any one of the configured databases. Since the error was recovered from, the exception is silenced at this level and not rethrown.

The last example illustrates incomplete recovery:


<?php

/*
 * loadConfig parses the provided configuration. If the configuration
 * is invalid, it will set the configuration to the default config.
 *
 */
function loadConfig(Config $conf) {
    try {
        $this->config = $conf->parse();
    } catch (Config_Parse_Exception e) {
        // Warn/Log code goes here
        // Perform incomplete recovery
        $this->config = $this->defaultConfig;
    }
}

?>

The recovery produces side effects, so it is considered incomplete. However, the program may proceed, so the exception is considered handled, and must not be rethrown. As in the previous example, when silencing the exception, logging or warning should occur.

Error Signaling in PHP5 PEAR packages

Error conditions in PEAR packages written for PHP5 must be signaled using exceptions. Usage of return codes or return PEAR_Error objects is deprecated in favor of exceptions. Naturally, packages providing compatibility with PHP4 do not fall under these coding guidelines, and may thus use the error handling mechanisms defined in the PHP4 PEAR coding guidelines.

An exception should be thrown whenever an error condition is met, according to the definition provided in the previous section. The thrown exception should contain enough information to debug the error and quickly identify the error cause. Note that, during production runs, no exception should reach the end-user, so there is no need for concern about technical complexity in the exception error messages.

The basic PEAR_Exception contains a textual error, describing the program state that led to the throw and, optionally, a wrapped lower level exception, containing more info on the lower level causes of the error.

The kind of information to be included in the Exception is dependent on the error condition. From the point of view of exception throwing, there are three classes of error conditions:

  1. Errors detected during precondition checks
  2. Lower level library errors signaled via error return codes or error return objects.
  3. Uncorrectable lower library exceptions.

Errors detected during precondition checks should contain a description of the failed check. If possible, the description should contain the violating value. Naturally, no wrapped exception can be included, as there isn't a lower level cause of the error. Example:


<?php

function divide($x,$y) {
    if ($y == 0) throw new Example_Aritmetic_Exception('Divide by zero');
    return $x/$y;
}

?>

Errors signaled via return codes by lower level libraries, if unrecoverable, should be turned into exceptions. The error description should try to convey all information contained in the original error. One example, is the connect method previously presented:


<?php

/*
 * Connect to Specified Database
 *
 * @throws Example_Datasource_Exception when it can't connect to specified DSN.
 */ 
function connectDB($dsn) {
    $this->db =& DB::connect($dsn);
    if (DB::isError($this->db)) {
        throw new Example_Datasource_Exception(
            "Unable to connect to $dsn:" . $this->db->getMessage()
        );
    }
}

?>

Lower library exceptions, if they can't be corrected, should either be rethrown or bubbled up. When rethrowing, the original exception must be wrapped inside the one being thrown. When letting the exception bubble up, the exception just isn't handled and will continue up the call stack in search of a handler.

One example for rethrowing:


<?php

function preTaxPrice($retailPrice, $taxRate) {
    try {
        return $this->divide($retailPrice, 1 + $taxRate);
    } catch (Example_Aritmetic_Exception e) {
        throw new Example_Tax_Exception('Invalid tax rate.', e);
    }
}

?>

And the same example for bubbling up:


<?php

function preTaxPrice($retailPrice, $taxRate) {
    return $this->divide($retailPrice, 1 + $taxRate);
}

?>

The case between rethrowing or bubbling up is one of software architecture: Exceptions should be bubbled up, except in these two cases:

  1. The original exception is from another package. Letting it bubble up would cause implementation details to be exposed, violating layer abstraction, conducing to poor design.
  2. The current method can add useful debugging information to the received error before rethrowing.

Exceptions and normal program flow

Exceptions should never be used as normal program flow. If removing all exception handling logic (try-catch statements) from the program, the remaining code should represent the "One True Path" -- the flow that would be executed in the absence of errors.

This requirement is equivalent to requiring that exceptions be thrown only on error conditions, and never in normal program states.

One example of a method that wrongly uses the bubble up capability of exceptions to return a result from a deep recursion:


<?php


/**
 * Recursively search a tree for string.
 * @throws ResultException
 */
public function search(TreeNode $node, $data)  {
    if ($node->data === $data) {
         throw new ResultException( $node );
    } else {
         search( $node->leftChild, $data );
         search( $node->rightChild, $data );
    }
}

?>

In the example the ResultException? is simply using the "eject!" qualities of exception handling to jump out of deeply nested recursion. When actually used to signify an error this is a very powerful feature, but in the example above this is simply lazy development.

Exception class hierarchies

All of PEAR packages exceptions must be descendant from PEAR_Exception. PEAR_Exception provides exception wrapping abilities, absent from the top level PHP Exception class, and needed to comply with the previous section requirements.

Aditionally, each PEAR package must provide a top level exception, named <Package_Name>_Exception. It is considered best practice that the package never throws Exceptions that aren't descendant from its top level exception.

Should this best practice be turned into a requirement? It's very practic for package users, as they can catch all of the package's exceptions in one go. However, it's a drag for package developers, who'll have to wrap all lower level exceptions.
-- Sérgio Carvalho

Exception silencing (Section placeholder)

This section will contain the requirements related to Exception silencing. Wether to support exception silencing or not is a cause for debate, and a vote will happen after (if) the main text approval. Supporting exception silencing, depending on PHP support for canceling throws, may require that an Exception throw be done via a PEAR_Exception method (PEAR_Exception::throw) which optionally silences the exception and allows the program to flow normally.

Documenting Exceptions

Because PHP, unlike Java, does not require you to explicitly state which Exceptions a method throws in the method signature, it is critical that Exceptions be thoroughly documented in your method headers.

Exceptions should be documented using the @throws phpdoc keyword:


<?php

/**
 * This method searches for aliens.
 * 
 * @return array Array of Aliens objects.
 * @throws AntennaBrokenException If the impedence readings indicate
 * that the antenna is broken.
 * 
 * @throws AntennaInUseException If another process is using the
 * antenna already.

 */
public function findAliens($color = 'green');
?>

In many cases middle layers of an application will rewrap any lower-level exceptions into more meaningful application exceptions. This also needs to be made clear:


<?php

/**
 * Load session objects into shared memory.
 * 
 * @throws LoadingException Any lower-level IOException will be wrapped
 * and re-thrown as a LoadingException.
 */
public function loadSessionObjects();
?>

In other cases your method may simply be a conduit through which lower level exceptions can pass freely. As challenging as it may be, your method should also document which exceptions it is not catching.


<?php

/**
 * Performs a batch of database queries (atomically, not in transaction).
 * @throws SQLException Low-level SQL errors will bubble up through this method.
 */
public function batchExecute();
?>

Exceptions as part of the API

Exceptions play a critical role in the API of your library. Developers using your library depend on accurate descriptions of where and why exceptions might be thrown from your package. Documentation is critical. Also maintaining the types of messages that are thrown is also an important requirement for maintaining backwards-compatibility.

Because Exceptions are critical to the API of your package, you must ensure that you don't break backwards compatibility by making changes to exceptions.

Things that break BC include:

Things that do not break BC:

Wed, 09 Mar 2005, 21:57