It appears to be still obscure to many web developers what different types of exceptions are for in PHP, so I’ve decided to write up some notes about the use and usage of them.
An exception indicates that something has failed. If your code doesn’t use exceptions too much, you may tend to only use PHP’s base Exception. But even PHP itself provides different types of exceptions, so what’s the use of them?
Subroutines can fail because of different reasons. A common, but bad thing is to handle all exceptions the same: Maybe log the error message somewhere, and then proceed as if nothing has happened. Or: blame all exceptions on the user – we all remember Windows error messages like “A fatal exception 0E has occurred at 0028 …” – and you asked yourself: “So, what can I do about it?”.
Obviously, different types of errors require different types of exceptions. But how to accomplish that in your code? First, let’s classify how things could go wrong. Let’s start with three categories: (1) The user has provided bad input, (2) an external system doesn’t behave correctly, (3) we have an internal problem. An example for category 1 would be a situation where form validation fails, category 2 would contain things such as failed HTTP calls, and category 3 would be everything else – because if we can’t blame it on external sources (and maybe not even name the cause), there is a mistake in our system.
Now we could create subcategories. For example, category 1 could contain type mismatches, set mismatches, or out-of-bound errors. Category 2 could contain connection errors, protocoll errors, or payload parsing errors. Category 3 could contain runtime errors, database connection errors, resource limit errors. (By the way, errors where earlier input causes output errors should be considered an internal error, because it is not the current user’s fault that we have accepted faulty input in the first place.) But let’s stay with our 3 categories.
To create an own type of exception, we simply write a class that is derived from the base Exception class (or any other exception class). Our exception class doesn’t even need a body to be useful.
class ExternalException extends Exception
{
}
Now if we create a try-catch-block, we may specifically look out for this exception and ignore all others:
try
{
$foo = getDataFromExternalService();
}
catch(ExternalException $e)
{
printf("The external call failed: %s", $e->getMessage());
}
This code will only catch ExternalException exceptions and potential children. All other exceptions are ignored and need to be caught in another place. Looking at the error message, we see the benefit of the dedicated exception: The catch part knows what type of error there has been, and it can be handled in place.
There’s another benefit of different exception types: We can process the error within the exception itself, for example log internal errors to a file:
class InternalException extends Exception
{
private $eNum = 0;
public function __construct($msg, $code=0, $prev=null)
{
parent::__construct($msg, $code, $prev);
$currentE = $this;
$prevE = null;
while ($currentE && $currentE != $prevE)
{
$this->logException($currentE);
$prevE = $currentE;
$currentE = $this->getPrevious();
}
}
private function logException(Exception $e)
{
file_put_contents(
'exceptions.log',
sprintf("EXCEPTION HISTORY, %s: %s\n%s", ++$this->eNum, $e->getMessage(), $e->getTraceAsString()),
FILE_APPEND);
}
}
This way, we don’t have to disclose internal exceptions to the user – it’s not of any use for them anyway. Let’s simply log it and output a generic “sorry” message.
try
{
// we assume that this goes well, otherwise it would throw an ExternalException
getDataFromExternalService();
// let's pretend the DB connection has failed
throw new InternalException('DB connection failed: ');
}
catch (ExternalException $eExternal)
{
printf("The external call failed: %s", $e->getMessage());
}
catch (InternalException $eInternal)
{
// message is logged in InternalException, but no need to disclose details
printf("Sorry, there has been an internal error. The administrators have been notified and will take care of this.");
}
As you certainly have guessed, the first catch is not executed, because it doesn’t match the exception type. The second catch will be executed, because there is an InternalException thrown in the try part.
The actual error with all details is logged inside our InternalException as declared above – but the user doesn’t need to, and will not, know about any of the details.