Error Handling
Every real-world application encounters errors: invalid user input, failed database connections, missing files, and network timeouts. PHP provides a powerful error handling system that lets you catch these problems gracefully instead of letting your application crash. Writing proper error handling code is what separates production-ready applications from fragile scripts.
What You'll Learn
- The difference between errors and exceptions
- Using try/catch/finally blocks
- Throwing exceptions and creating custom exception classes
- PHP's built-in exception hierarchy
- Error reporting levels and configuration
- Custom error handlers with
set_error_handler
Try, Catch, and Finally
The try/catch block is the primary way to handle exceptions in PHP. Code that might fail goes in the try block, and your recovery logic goes in one or more catch blocks:
<?php
function divide(float $a, float $b): float {
if ($b === 0.0) {
throw new InvalidArgumentException("Division by zero is not allowed");
}
return $a / $b;
}
// Basic try/catch
try {
echo "10 / 2 = " . divide(10, 2) . "\n";
echo "10 / 0 = " . divide(10, 0) . "\n";
echo "This line never runs\n";
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
echo "\n";
// Try/catch/finally
function readConfig(string $filename): string {
if (!file_exists($filename)) {
throw new RuntimeException("Config file not found: {$filename}");
}
return file_get_contents($filename);
}
try {
$config = readConfig("/nonexistent/config.json");
echo "Config loaded\n";
} catch (RuntimeException $e) {
echo "Caught: " . $e->getMessage() . "\n";
} finally {
echo "This always runs, whether or not an exception occurred\n";
}
When an exception is thrown, PHP immediately stops executing the try block and jumps to the matching catch block. The finally block always executes, regardless of whether an exception was thrown, making it ideal for cleanup tasks.
Catching Multiple Exception Types
You can catch different exception types with separate catch blocks, or use the union type syntax to handle multiple types in one block:
<?php
function processInput(mixed $value): string {
if (!is_string($value) && !is_int($value)) {
throw new TypeError("Expected string or int, got " . gettype($value));
}
if (is_string($value) && strlen($value) === 0) {
throw new InvalidArgumentException("String cannot be empty");
}
if (is_int($value) && $value < 0) {
throw new RangeException("Integer must be non-negative, got {$value}");
}
return "Processed: {$value}";
}
// Separate catch blocks
$testValues = ["hello", "", -5, 3.14, 42];
foreach ($testValues as $value) {
try {
echo processInput($value) . "\n";
} catch (TypeError $e) {
echo "Type error: " . $e->getMessage() . "\n";
} catch (InvalidArgumentException $e) {
echo "Invalid argument: " . $e->getMessage() . "\n";
} catch (RangeException $e) {
echo "Range error: " . $e->getMessage() . "\n";
}
}
echo "\n";
// Union catch (PHP 8.0+): catch multiple types in one block
try {
processInput([1, 2, 3]);
} catch (TypeError | InvalidArgumentException $e) {
echo "Input problem: " . $e->getMessage() . "\n";
} catch (RangeException $e) {
echo "Range problem: " . $e->getMessage() . "\n";
}
PHP matches catch blocks from top to bottom, using the first one whose type matches the thrown exception. Place more specific exception types before general ones.
Custom Exceptions
Creating custom exception classes gives you meaningful, domain-specific error types. This makes it easy to distinguish between different failure modes:
<?php
class ValidationException extends RuntimeException {
private array $errors;
public function __construct(array $errors, int $code = 0, ?\Throwable $previous = null) {
$this->errors = $errors;
$message = "Validation failed: " . implode(", ", $errors);
parent::__construct($message, $code, $previous);
}
public function getErrors(): array {
return $this->errors;
}
}
class InsufficientFundsException extends RuntimeException {
public function __construct(
private float $balance,
private float $amount,
int $code = 0,
?\Throwable $previous = null
) {
$message = "Cannot withdraw \${$amount} from balance of \${$balance}";
parent::__construct($message, $code, $previous);
}
public function getBalance(): float {
return $this->balance;
}
public function getAmount(): float {
return $this->amount;
}
}
// Using custom exceptions
function validateUser(array $data): void {
$errors = [];
if (empty($data['name'])) {
$errors[] = "Name is required";
}
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = "Valid email is required";
}
if (empty($data['age']) || $data['age'] < 18) {
$errors[] = "Must be at least 18 years old";
}
if (!empty($errors)) {
throw new ValidationException($errors);
}
}
// Test validation
$testData = [
['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 25],
['name' => '', 'email' => 'bad-email', 'age' => 15],
];
foreach ($testData as $data) {
try {
validateUser($data);
echo "User valid: {$data['name']}\n";
} catch (ValidationException $e) {
echo "Validation failed:\n";
foreach ($e->getErrors() as $error) {
echo " - {$error}\n";
}
}
}
Custom exceptions can carry extra data (like the $errors array above) beyond just a message. Extend RuntimeException for errors that occur during execution or LogicException for programming errors that should be fixed in code.
Error Reporting and Levels
PHP has a legacy error system alongside exceptions. Understanding error levels helps you configure reporting properly:
<?php
// Error reporting levels (common ones)
// E_ERROR - Fatal errors (script stops)
// E_WARNING - Non-fatal warnings
// E_NOTICE - Minor notices (undefined variable, etc.)
// E_DEPRECATED - Deprecation warnings
// E_ALL - All errors and warnings
// Show all errors (good for development)
error_reporting(E_ALL);
// Convert warnings to exceptions using set_error_handler
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
// Convert all errors to ErrorException
throw new ErrorException($message, 0, $severity, $file, $line);
});
// Now warnings become catchable exceptions
try {
// This would normally be a warning, not an exception
$result = intdiv(10, 0);
} catch (ErrorException $e) {
echo "Caught converted error: " . $e->getMessage() . "\n";
} catch (DivisionByZeroError $e) {
echo "Caught division by zero: " . $e->getMessage() . "\n";
}
// Restore default error handler
restore_error_handler();
// Custom error handler for logging
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
$level = match ($severity) {
E_WARNING => 'WARNING',
E_NOTICE => 'NOTICE',
E_DEPRECATED => 'DEPRECATED',
default => 'ERROR',
};
echo "[{$level}] {$message} in {$file}:{$line}\n";
return true; // Return true to prevent default PHP handler
});
echo "Error handler is now active\n";
restore_error_handler();
echo "Default error handler restored\n";
The set_error_handler function lets you intercept PHP errors and handle them your way. Converting errors to exceptions (using ErrorException) is a popular pattern because it lets you use try/catch everywhere instead of mixing two different error systems.
Practical Error Handling Pattern
Here is a real-world pattern that combines custom exceptions, error handling, and proper resource cleanup:
<?php
class AppException extends RuntimeException {}
class HttpException extends AppException {
public function __construct(
private int $statusCode,
string $message = "",
?\Throwable $previous = null
) {
parent::__construct($message, $statusCode, $previous);
}
public function getStatusCode(): int {
return $this->statusCode;
}
}
class NotFoundException extends HttpException {
public function __construct(string $resource, ?\Throwable $previous = null) {
parent::__construct(404, "{$resource} not found", $previous);
}
}
// Simulated application logic
function findUser(int $id): array {
$users = [
1 => ['name' => 'Alice', 'email' => 'alice@example.com'],
2 => ['name' => 'Bob', 'email' => 'bob@example.com'],
];
if (!isset($users[$id])) {
throw new NotFoundException("User #{$id}");
}
return $users[$id];
}
function handleRequest(int $userId): void {
try {
$user = findUser($userId);
echo "200 OK: Found {$user['name']} ({$user['email']})\n";
} catch (NotFoundException $e) {
echo "{$e->getStatusCode()} Not Found: {$e->getMessage()}\n";
} catch (HttpException $e) {
echo "{$e->getStatusCode()} Error: {$e->getMessage()}\n";
} catch (Throwable $e) {
// Catch-all for unexpected errors
echo "500 Internal Error: Something went wrong\n";
// In production, log the actual error:
// error_log($e->getMessage());
}
}
handleRequest(1);
handleRequest(2);
handleRequest(99);
This pattern creates a hierarchy of exception classes that mirrors HTTP status codes. The catch-all Throwable block at the bottom ensures no error goes unhandled in production.
Key Takeaways
- Use try/catch/finally to handle exceptions gracefully
- The
finallyblock always runs, making it ideal for cleanup - Create custom exception classes for domain-specific errors
- PHP 8 union types let you catch multiple exception types in one block:
catch (TypeA | TypeB $e) - Use
set_error_handlerto convert legacy PHP errors into exceptions - Place specific catch blocks before general ones
- Always have a catch-all for unexpected errors in application entry points
- Use
ErrorExceptionto bridge the gap between legacy errors and exceptions
Next Steps
You can now catch exceptions with try/catch/finally, throw custom exceptions with domain-specific context, and convert legacy PHP errors into the exception system. Your code can fail gracefully instead of crashing.
These error handling skills are about to become essential. Most PHP applications store and retrieve data from a database, and database operations are among the most error-prone parts of any application -- connections time out, queries fail, constraints are violated. Working with databases teaches you PDO, prepared statements, and transactions. You will use try/catch on nearly every database call, making this lesson a direct extension of what you just learned.
Continue to Working with Databases -->
Pro Tip: Never expose internal error details to end users in production. Log detailed error information server-side using
error_log()or a logging library, and show users a friendly, generic message. An attacker can use detailed error messages to learn about your application's internals. Configuredisplay_errors = Offin your production php.ini.