Errors and Exceptions in PHP

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

PHP 8.4

PHP has two mechanisms: errors for engine-level problems and exceptions for problems your code raises deliberately. Knowing which is which decides whether you catch it or fix it.

Error levels

Level Typical cause Fatal
E_ERROR Fatal engine error, e.g. calling an undefined function yes
E_WARNING Runtime warning, e.g. a failed include no
E_NOTICE Undefined variable or array key no
E_DEPRECATED Feature that will be removed no
E_PARSE Syntax error yes

PHP 8 promoted many historical errors to Error exceptions (TypeError, DivisionByZeroError, ValueError), so try / catch now covers most failures.

The Throwable hierarchy

Throwable
├── Exception          (thrown by application code)
│   ├── RuntimeException
│   ├── LogicException
│   └── ...
└── Error              (engine-level, PHP 7+)
    ├── TypeError
    ├── ValueError
    ├── DivisionByZeroError
    └── ParseError

Catching Throwable covers both, but business code should normally catch Exception — an engine-level Error usually means a bug you want to see, not swallow.

try / catch / finally

try {
    $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    error_log("Parse failed: " . $e->getMessage());
    $data = [];
} finally {
    // always runs: close handles, restore state
}

finally runs whether or not an exception occurred, and even if the catch block returns.

Order multiple catch blocks from specific to general:

try {
    process($input);
} catch (InvalidArgumentException $e) {
    echo "Bad input: " . $e->getMessage();
} catch (RuntimeException $e) {
    echo "Something went wrong: " . $e->getMessage();
} catch (Throwable $e) {
    error_log($e);
    echo "Internal server error";
}

What an exception carries

try {
    throw new RuntimeException("Connection timed out");
} catch (RuntimeException $e) {
    $e->getMessage();        // "Connection timed out"
    $e->getCode();           // 0 by default
    $e->getFile();           // file where it was thrown
    $e->getLine();           // line number
    $e->getTraceAsString();  // stack trace
}

Throwing

function withdraw(float $balance, float $amount): float
{
    if ($amount <= 0) {
        throw new InvalidArgumentException("Withdrawal amount must be positive");
    }
    if ($amount > $balance) {
        throw new RuntimeException("Insufficient funds", 1001);
    }
    return $balance - $amount;
}

Which class to pick:

  • InvalidArgumentException — the caller passed something wrong (a LogicException, i.e. a code bug).
  • RuntimeException — an external condition failed: no balance, missing file, network timeout.
  • DomainException — a value is outside the domain the business allows.

Custom exceptions

Once error handling branches on type, a dedicated class beats string matching:

class ValidationException extends RuntimeException
{
    /** @param array<string, string> $errors field => message */
    public function __construct(public array $errors)
    {
        parent::__construct("Input validation failed");
    }
}

try {
    $errors = validate($_POST);
    if ($errors !== []) {
        throw new ValidationException($errors);
    }
} catch (ValidationException $e) {
    foreach ($e->errors as $field => $message) {
        echo htmlspecialchars($field, ENT_QUOTES, "UTF-8") . ": "
           . htmlspecialchars($message, ENT_QUOTES, "UTF-8") . "<br>";
    }
}

Constructor property promotion (public array $errors) declares and assigns the property in one step.

Making PHP functions throw instead of returning false

Many older functions signal failure with false. Some have a flag that converts that into an exception:

// default: null on failure, then json_last_error() to find out why
$data = json_decode($raw, true);

// PHP 7.3+: throws JsonException
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);

PDO has the equivalent PDO::ERRMODE_EXCEPTION. filter_var still needs an explicit === false check.

A global handler as the last line of defence

An uncaught exception becomes a fatal error; a handler lets you log it and show something friendly:

set_exception_handler(function (Throwable $e): void {
    error_log(sprintf(
        "[%s] %s in %s:%d\n%s",
        date("c"),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    http_response_code(500);
    echo "Something went wrong. Please try again later.";
});

Register it at the very top of your entry point (index.php). It only handles exceptions that nothing else caught.

Error reporting configuration

Development — show everything:

; php.ini — development
display_errors = On
error_reporting = E_ALL
log_errors = On

Production — log it, never display it (messages can leak paths, SQL and credentials):

; php.ini — production
display_errors = Off
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php/error.log

Per-script overrides are possible too:

error_reporting(E_ALL);
ini_set("display_errors", "0");

Common mistakes

1. An empty catch (Exception $e) {}

The failure is swallowed. At minimum call error_log.

2. Catching Throwable and returning a default

That hides engine-level bugs along with expected failures. Catch the types you can actually handle.

3. Using @ to silence errors

The message disappears but execution continues in a broken state.

4. Leaving display_errors on in production

Paths, queries and credentials end up in the browser.

5. Returning from finally

It overrides the value returned by try or catch, almost never what you meant.

6. Printing $e->getMessage() to users

Log the detail, show a generic message.

Summary

  • Exception for business problems, Error for engine problems, Throwable is their parent.
  • Throw InvalidArgumentException for bad arguments, RuntimeException for external conditions.
  • Prefer flags like JSON_THROW_ON_ERROR over checking return values.
  • Register set_exception_handler at the entry point for a uniform fallback.
  • Production: display_errors = Off, log_errors = On.

Next: sessions and cookies — how PHP remembers a user across requests.