PHP Error Handling

Catch problems gracefully with try/catch/finally, throw meaningful exceptions, define custom exception classes, and set up global error handlers for production apps.

Intermediate 9 min read 10 examples

Errors vs Exceptions

Modern PHP unifies both under the Throwable interface:

Throwable (interface)
├── Error           (engine-level: TypeError, ParseError, ...)
└── Exception       (user-thrown: RuntimeException, LogicException, ...)

try / catch / finally

PHP
<?php
$pdo = new PDO("sqlite:db.sqlite");

try {
    $pdo->beginTransaction();
    $pdo->exec("INSERT INTO users (name) VALUES ('Alice')");
    $pdo->exec("INSERT INTO orders (user_id, total) VALUES (1, 99)");
    $pdo->commit();
    echo "Success";
} catch (PDOException $e) {
    $pdo->rollBack();
    echo "Database error: " . $e->getMessage();
} finally {
    // Always runs - even after return or rethrow
    $pdo = null;
}

Exception Hierarchy

ClassUse for
RuntimeExceptionErrors that arise only at runtime (network, DB, file)
LogicExceptionBugs that should be caught in development
InvalidArgumentExceptionBad argument passed to a function
OutOfRangeExceptionIndex out of range
TypeErrorWrong type for parameter or return (engine-thrown)
ValueErrorValue out of expected set (PHP 8.0+)
UnhandledMatchErrormatch() arm didn\'t match (PHP 8.0+)

Throwing Exceptions

PHP
<?php
function divide(int $a, int $b): float {
    if ($b === 0) {
        throw new InvalidArgumentException("Division by zero");
    }
    return $a / $b;
}

// Throw is now an expression (PHP 8.0+)
$user = $repo->find($id) ?? throw new NotFoundException("User $id not found");

Multi-catch (PHP 8.0+)

PHP
<?php
try {
    $data = json_decode(file_get_contents($path), flags: JSON_THROW_ON_ERROR);
} catch (RuntimeException | JsonException $e) {
    // handle both file errors and JSON errors the same way
    logger()->error($e->getMessage());
}

// Catch without variable - "I dont need details"
try {
    $cache->refresh();
} catch (Throwable) {
    // best effort - ignore failures
}

Custom Exception Classes

PHP
<?php
namespace App\Exception;

class NotFoundException extends \RuntimeException {
    public function __construct(
        public readonly string $resource,
        public readonly int|string $id,
    ) {
        parent::__construct("$resource #$id not found", 404);
    }
}

class ValidationException extends \RuntimeException {
    public function __construct(public readonly array $errors) {
        parent::__construct("Validation failed", 422);
    }
}

// Usage
throw new NotFoundException("User", 42);

try {
    // ...
} catch (NotFoundException $e) {
    http_response_code($e->getCode());
    echo json_encode(["error" => $e->getMessage(), "id" => $e->id]);
}

Error Reporting & Logging

PHP
<?php
// Development - show everything
error_reporting(E_ALL);
ini_set("display_errors", "1");
ini_set("log_errors", "1");
ini_set("error_log", __DIR__ . "/error.log");

// Production - log only, never display to users
error_reporting(E_ALL);
ini_set("display_errors", "0");
ini_set("log_errors", "1");
ini_set("error_log", "/var/log/app/php-errors.log");

// Log directly
error_log("Payment failed for user $userId", 3, "payments.log");
NEVER display errors in production

Stack traces leak file paths, query syntax, and sometimes credentials. display_errors = Off in production. Log to a file/service and show users a generic error page.

Global Error/Exception Handlers

PHP
<?php
// Convert PHP warnings/notices into exceptions
set_error_handler(function ($severity, $message, $file, $line) {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// Catch anything that escapes top-level
set_exception_handler(function (Throwable $e) {
    error_log($e);
    http_response_code(500);
    echo "Internal Server Error";
});

// Catch fatal errors (parse/memory) - last chance
register_shutdown_function(function () {
    $err = error_get_last();
    if ($err && in_array($err["type"], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
        error_log("FATAL: " . $err["message"]);
    }
});

Best Practices

  1. Throw early, catch late. Validate at boundaries and catch at the request/CLI boundary.
  2. Catch specific exception classes when you can recover from them.
  3. Always log - never swallow exceptions silently.
  4. Don't use exceptions for control flow - they're slow and obscure intent.
  5. Chain exceptions with new HighLevelException("msg", 0, $e) to preserve the cause.
  6. Use finally for cleanup that must always run.

Next Steps

Frequently Asked Questions

In modern PHP (7+), both are Throwable and can be caught. Historically, errors were low-level engine problems (parse errors, fatal errors) and exceptions were thrown by user code. Today the line is blurred - catch Throwable to catch everything.

For top-level error handlers (web request boundary, CLI exit handler), catch Throwable so you don't miss Error subclasses like TypeError. For business logic, catch the specific exception class you can recover from.

Almost never. Either handle the error (recover) or rethrow it (let the caller decide). Silent catch (Exception $e) {} hides bugs - at minimum log the exception so you can find out what went wrong later.

When callers need to distinguish your error from generic ones, or when you want to attach context (a user ID, an HTTP status). A dozen well-named exceptions beat one giant RuntimeException with cryptic messages.

Yes - even if you return from try or throw an uncaught exception. Use it for cleanup that must happen no matter what: closing files, releasing locks, ending DB transactions.