PHP exceptions are the difference between a page that fails politely and one that stops dead halfway through with its footer missing. A missing CSV prints a yellow warning and carries on. A dead database connection throws โ and unless something catches it, the script ends on that line.
This lesson works through eight files, each one a single idea: the difference between a warning and a throw, catching one, reading what the caught object knows, throwing your own, why catch order matters, what finally guarantees, writing an exception class that carries your own data, and the one that catches most people out โ why catch (Exception $e) quietly fails to catch a division by zero.
Every output below is a real run on PHP 8.4.21. The two pages that end in a red fatal error end there on purpose โ with PHP exceptions, a page dying is sometimes exactly what you are meant to see.
A warning is a note, an exception is a stop
Start with the two side by side. file() on a missing CSV raises a warning, hands back false, and lets the script reach the next line. new PDO() on a port where nothing is listening throws โ and because nothing catches it, the script ends right there.
Port 3307 is deliberate. Nothing is listening on it, so the connection fails on any machine whether or not MySQL is running.
<?php
// A warning: PHP complains, hands back false, and carries on
$rows = file('sales-2019.csv');
var_dump($rows);
echo "The script is still running\n";
// An exception: PHP stops right here unless something catches it
$pdo = new PDO('mysql:host=127.0.0.1;port=3307;dbname=shop', 'root', '');
echo "This line never runs\n";
Output:
Read the order. The warning prints, var_dump() shows bool(false), and The script is still running appears โ PHP complained and carried on. Then the new PDO() line throws, and This line never runs never prints. That last absence is the whole point: an uncaught exception does not return a value you can test, it ends the script.
try and catch turn a dead page into a message
Same connection, same dead port. The only change is that the risky line now sits inside a try block with a catch after it.
<?php
try {
$pdo = new PDO('mysql:host=127.0.0.1;port=3307;dbname=shop', 'root', '');
echo "Connected to the shop database\n";
} catch (PDOException $e) {
echo "The shop is offline for a moment. Please try again shortly.\n";
}
echo "The page carries on and can still draw a menu and a footer\n";
Output:
Two things changed and neither of them is the database. Connected to the shop database never prints, because the throw happens on the line above it and the rest of the try block is abandoned. And the line after the whole try/catch does print โ the script survived. Nothing about the failure was fixed; only the handling was. That is what PHP exceptions buy you: a choice about what happens next.
getMessage, getCode, getFile and getLine
The variable in catch (PDOException $e) is an object, and it knows a surprising amount about what went wrong.
<?php
try {
$pdo = new PDO('mysql:host=127.0.0.1;port=3307;dbname=shop', 'root', '');
} catch (PDOException $e) {
echo 'Class: ' . get_class($e) . "\n";
echo 'Message: ' . $e->getMessage() . "\n";
echo 'Code: ' . $e->getCode() . "\n";
echo 'File: ' . $e->getFile() . "\n";
echo 'Line: ' . $e->getLine() . "\n";
}
Output:
File and Line point at where the exception was thrown, not at the catch block that caught it. Line 11 is the new PDO() call, several lines above the echo that printed it. That is what makes these two useful in a log: they name the failing line, not the handling code.
Message and Code come from the driver. SQLSTATE[HY000] and MySQL error 2002 both mean the same thing โ nothing answered. The sentence after [2002] comes from the operating system, so it reads differently on Windows and Linux; the part worth matching on is the SQLSTATE[HY000] [2002] fragment, which is identical everywhere.
throw stops the function on the spot
Exceptions are not only for things PHP throws at you. throw is how your own function refuses to do something.
<?php
function chargeCard($amount)
{
if ($amount <= 0) {
throw new InvalidArgumentException('Amount must be more than zero');
}
return 'Charged ' . number_format($amount, 2);
}
echo chargeCard(19.99), "\n";
try {
echo chargeCard(0), "\n";
} catch (InvalidArgumentException $e) {
echo 'Refused: ' . $e->getMessage() . "\n";
}
Output:
The first call returns normally. The second throws before it reaches return, so chargeCard() hands nothing back at all โ and because there is no return value, the echo wrapped around it never runs either. The catch block prints instead.
That is the difference between throwing and returning false. A false can be ignored by a caller who forgets to check it. A throw cannot โ which is why PHP exceptions are the better way for a function to refuse bad input.
Order decides which catch block wins
You can list several catch blocks. PHP takes the first one whose class matches โ and a parent class matches every one of its children.
<?php
function loadConfig($file)
{
if (!is_file($file)) {
throw new RuntimeException("Config file $file is missing");
}
return parse_ini_file($file);
}
// Wrong order: Exception is the parent of RuntimeException, so it wins
try {
loadConfig('settings.ini');
} catch (Exception $e) {
echo 'General block caught a ' . get_class($e) . "\n";
} catch (RuntimeException $e) {
echo "Specific block never runs\n";
}
// Right order: the narrowest class first
try {
loadConfig('settings.ini');
} catch (RuntimeException $e) {
echo 'Specific block: ' . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "General block never runs for this one\n";
}
// Multi-catch: one block, two unrelated classes (PHP 7.1 and newer)
foreach ([new RuntimeException('Disk full'), new InvalidArgumentException('Bad id')] as $problem) {
try {
throw $problem;
} catch (RuntimeException | InvalidArgumentException $e) {
echo 'Multi-catch handled ' . get_class($e) . ': ' . $e->getMessage() . "\n";
}
}
Output:
Both try blocks throw exactly the same RuntimeException. The first prints from the general block, because Exception is listed first and RuntimeException is a kind of Exception. The second prints from the specific block, because that one is listed first.
Note what PHP does not do: it never warns you that Specific block never runs is unreachable. Put the broad class first and your careful handling is silently dead code. Narrowest first, always.
The last loop is multi-catch โ one block handling two unrelated classes, separated by |. It needs PHP 7.1 or newer.
Loading comments...