PHP Exceptions
What Is an Exception? In simple terms: An exception is a way for code to say "something went wrong, and I cannot continue normally". Instead of returning a special value that the caller might forget t
What Is an Exception?
In simple terms: An exception is a way for code to say "something went wrong, and I cannot continue normally". Instead of returning a special value that the caller might forget to check, the code throws an exception, and the nearest code prepared to deal with it catches it.
Technically: An exception is an object of a class that implements the Throwable interface. throw stops the current execution and transfers control to the first matching catch block up the call stack. If no catch matches, the script ends with a fatal "Uncaught exception" error.
Why Exceptions Instead of Return Values?
$user = findUser($id); // returns false on error... or null... or an empty array?
With return values, every caller must remember to check for every failure value, and errors are easily ignored. Exceptions:
- cannot be silently ignored — an unhandled exception stops the script with a clear message,
- separate normal code from error handling,
- travel up through several function calls to the place that can actually handle the problem,
- carry information: a message, a code, the location, a stack trace, and any custom data.
Throwing and Catching
Syntax
try {
// code that may throw
} catch (SomeException $e) {
// handle the problem
} finally {
// always runs (optional)
}
Example
<?php
function divide(int $a, int $b): float
{
if ($b === 0) {
throw new InvalidArgumentException('Cannot divide by zero.');
}
return $a / $b;
}
try {
echo divide(10, 4) . "\n";
echo divide(5, 0) . "\n";
echo "This line is never reached\n";
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
echo "The program continues\n";
Output
2.5
Error: Cannot divide by zero.
The program continues
How It Works
divide(10, 4)returns normally.divide(5, 0)executesthrow. The function stops immediately; nothing afterthrowruns.- PHP looks for a matching
catchblock. The remaining lines in thetryblock are skipped. - The
catchblock receives the exception object in$eand handles it. - Execution continues after the
try/catchstatement.
The Exception Hierarchy
Throwable (interface)
├── Error ← problems detected by the PHP engine
│ ├── TypeError
│ │ └── ArgumentCountError
│ ├── ValueError
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── UnhandledMatchError
│ └── ...
└── Exception ← problems in application logic
├── LogicException
│ ├── InvalidArgumentException
│ ├── DomainException
│ ├── LengthException
│ └── OutOfRangeException
├── RuntimeException
│ ├── OutOfBoundsException
│ ├── UnexpectedValueException
│ ├── RangeException
│ └── ...
├── JsonException
└── ...
| Branch | Meaning | Examples |
|---|---|---|
Error | Programming errors and engine failures | Wrong argument types, division by zero, calling undefined functions |
LogicException | A bug in the code that should be fixed | Invalid arguments passed by a programmer |
RuntimeException | Problems that occur only at runtime | Missing files, network failures, unavailable services |
Catching Engine Errors
<?php
$operations = [
'intdiv' => fn() => intdiv(10, 0),
'modulo' => fn() => 10 % 0,
'type' => fn() => str_repeat('x', 'three'),
'value' => fn() => str_repeat('x', -1),
];
foreach ($operations as $name => $operation) {
try {
$operation();
} catch (Throwable $e) {
echo str_pad($name, 7) . get_class($e) . ": " . $e->getMessage() . "\n";
}
}
Output
intdiv DivisionByZeroError: Division by zero
modulo DivisionByZeroError: Modulo by zero
type TypeError: str_repeat(): Argument #2 ($times) must be of type int, string given
value ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0
catch (Throwable $e) catches everything — both Error and Exception. Use it at the top level of an application (to log and show an error page), not around ordinary code, where it could hide bugs.
Multiple and Combined catch Blocks
<?php
function loadSettings(string $json): array
{
if ($json === '') {
throw new LengthException('Settings are empty.');
}
return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
}
foreach (['{"theme":"dark"}', '', '{bad json}'] as $input) {
try {
$settings = loadSettings($input);
echo "Theme: {$settings['theme']}\n";
} catch (LengthException | JsonException $e) {
echo "Invalid settings (" . get_class($e) . "): " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Unexpected problem: " . $e->getMessage() . "\n";
}
}
Output
Theme: dark
Invalid settings (LengthException): Settings are empty.
Invalid settings (JsonException): Syntax error
catch (A | B $e)(PHP 7.1+) handles several exception types with the same code.catchblocks are checked in order, and the first matching one runs. Put specific types before general ones such asException; otherwise the general block catches everything first.- Since PHP 8.0, the variable can be omitted when it is not needed:
catch (JsonException) { ... }.
finally: Code That Always Runs
<?php
function processFile(string $path): string
{
$handle = fopen('php://memory', 'r+');
echo "File opened\n";
try {
if ($path === 'bad') {
throw new RuntimeException('Processing failed');
}
return 'Processed';
} finally {
fclose($handle);
echo "File closed\n";
}
}
echo processFile('good') . "\n";
try {
processFile('bad');
} catch (RuntimeException $e) {
echo "Caught: " . $e->getMessage() . "\n";
}
Output
File opened
File closed
Processed
File opened
File closed
Caught: Processing failed
The finally block runs whether the try block finishes normally, returns, or throws. It is the right place for cleanup: closing files, releasing locks, or resetting state. Note that in the first call, finally runs before the returned value reaches the caller.
Custom Exceptions
Creating your own exception classes lets callers catch specific problems and carry extra information:
<?php
final class InsufficientFundsException extends RuntimeException
{
public function __construct(
public readonly float $balance,
public readonly float $requested,
) {
parent::__construct(sprintf(
'Cannot withdraw ₹%s: balance is ₹%s.',
number_format($requested, 2),
number_format($balance, 2)
));
}
public function shortfall(): float
{
return $this->requested - $this->balance;
}
}
function withdraw(float $balance, float $amount): float
{
if ($amount > $balance) {
throw new InsufficientFundsException($balance, $amount);
}
return $balance - $amount;
}
try {
withdraw(1000, 1500);
} catch (InsufficientFundsException $e) {
echo $e->getMessage() . "\n";
echo "Please add ₹" . number_format($e->shortfall(), 2) . " to continue.\n";
}
Output
Cannot withdraw ₹1,500.00: balance is ₹1,000.00.
Please add ₹500.00 to continue.
Guidelines for Custom Exceptions
- Extend the most fitting built-in class (
RuntimeException,InvalidArgumentException,DomainException). - Name them after the problem:
InsufficientFundsException,OrderNotFoundException. - Add properties or methods for data that handlers need (here, the balance and the shortfall).
- Libraries often define a common base exception (for example,
PaymentException), so users can catch all of the library's exceptions at once.
Exception Chaining: Keeping the Original Cause
When converting a low-level exception into a higher-level one, pass the original as the previous exception:
<?php
final class SettingsUnavailableException extends RuntimeException {}
function loadUserSettings(string $json): array
{
try {
return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new SettingsUnavailableException('User settings could not be loaded.', 0, $e);
}
}
try {
loadUserSettings('{broken');
} catch (SettingsUnavailableException $e) {
echo $e->getMessage() . "\n";
echo "Caused by: " . get_class($e->getPrevious()) . ": " . $e->getPrevious()->getMessage() . "\n";
}
Output
User settings could not be loaded.
Caused by: JsonException: Syntax error
Callers deal with a meaningful, domain-level exception, while logs still contain the technical root cause. Logging libraries and error pages display the whole chain.
Useful Exception Methods
| Method | Returns |
|---|---|
getMessage() | The message |
getCode() | The numeric code (default 0) |
getPrevious() | The previous exception, or null |
getFile() / getLine() | Where the exception was created |
getTrace() / getTraceAsString() | The call stack at that point |
throw as an Expression (PHP 8.0+)
throw can be used where an expression is expected, for example with ??, ?:, and arrow functions:
<?php
$users = [7 => 'Asha', 9 => 'Bala'];
function userName(array $users, int $id): string
{
return $users[$id] ?? throw new OutOfBoundsException("User $id not found.");
}
echo userName($users, 7) . "\n";
try {
echo userName($users, 42) . "\n";
} catch (OutOfBoundsException $e) {
echo $e->getMessage() . "\n";
}
Output
Asha
User 42 not found.
Where to Catch Exceptions
- Throw where a problem is detected.
- Catch only where you can do something useful: retry, use a fallback, show a helpful message, or convert the exception into a more meaningful one.
- Let other exceptions propagate to a global handler that logs them and shows a generic error page (see Error Handling and Custom Error Handler).
An empty catch block — catch (Exception $e) {} — hides problems and makes debugging very difficult. If an exception is truly ignorable, add a comment explaining why.
Common Mistakes
Catching Too Broadly
Effect: catch (Throwable) around ordinary code hides programming errors.
Fix: Catch specific types; handle Throwable only at the top level.
Wrong Order of catch Blocks
Effect: A general catch (Exception) placed first prevents specific blocks from running.
Fix: Order catch blocks from specific to general.
Swallowing Exceptions
Effect: Errors disappear silently.
Fix: Handle, log, or rethrow — never ignore without a reason.
Losing the Original Exception
Effect: Logs show only the high-level message, not the root cause.
Fix: Pass the original as $previous.
Using Exceptions for Normal Control Flow
Effect: Hard-to-follow code and unnecessary overhead.
Fix: Use exceptions for exceptional situations; use return values (for example, null for "not found" where that is a normal outcome).
Best Practices
- Throw exceptions for situations a function cannot handle itself.
- Create meaningful custom exception classes that carry useful data.
- Catch specific exceptions where you can handle them; let others propagate.
- Use
finallyfor cleanup and exception chaining to preserve root causes. - Install a global handler that logs uncaught exceptions and shows a safe error page.
Official reference: https://www.php.net/manual/en/language.exceptions.php