Errors & Exceptions

PHP · Lesson 39 of 50 7 min read Updated

Things go wrong in every real program. A customer types 0 people when splitting a bill, a settings file is broken, the database server is down. A good program does not crash with a scary message when this happens. It notices the problem, deals with it, and shows the visitor something sensible.

In this lesson you will catch problems with try, catch and finally, throw exceptions of your own, design small custom exception classes, and set up error handling that is helpful during development and safe in production.

try, catch and finally

An exception is an object that PHP throws when something goes wrong. Throwing stops the normal flow of the code. If some code higher up has caught that kind of exception, PHP jumps there. If nobody catches it, the script stops with a fatal error.

PHP
<?php
declare(strict_types=1);

function splitBill(int $total, int $people): int
{
    return intdiv($total, $people);
}

try {
    echo 'Each pays ₹' . splitBill(1200, 4) . "\n";
    echo 'Each pays ₹' . splitBill(1200, 0) . "\n";
    echo "This line never runs\n";
} catch (DivisionByZeroError $e) {
    echo 'Problem: ' . $e->getMessage() . "\n";
} finally {
    echo "Bill calculation finished\n";
}

echo "The script carries on\n";
Output
Each pays ₹300
Problem: Division by zero
Bill calculation finished
The script carries on
  • The try block holds code that might fail.
  • When intdiv() throws, PHP skips the rest of the try block and runs the matching catch block. $e is the exception object, and getMessage() returns its message.
  • The finally block runs in every case, whether or not something was thrown. It is the place for clean-up, such as closing a file.
  • After the catch, the script continues normally.

Useful methods on every exception: getMessage(), getCode(), getFile(), getLine(), getTrace() and getPrevious().

Error and Exception: the family tree

Everything that can be thrown implements the Throwable interface, and it splits into two branches:

Text
Throwable
  Error                         mistakes in the code itself
    TypeError                   wrong type passed (with strict_types)
      ArgumentCountError        too few arguments
    ValueError                  right type, impossible value
    ArithmeticError
      DivisionByZeroError
  Exception                     problems a program should expect
    LogicException
      InvalidArgumentException
    RuntimeException
      OutOfBoundsException
      PDOException
    JsonException

Error usually means a bug: calling a function with the wrong type, or reading an uninitialized property. Fix the code rather than catching these. Exception means a problem that can happen even in correct code: bad user input, a missing file, a dropped database connection. These are the ones you catch and handle.

catch (Exception $e) catches every Exception but no Error. catch (Throwable $e) catches both; use it only at the very top level of an app, as shown later.

Throwing your own exceptions

Use throw when your function receives something it cannot work with. Pick the built-in class that describes the problem, or write your own by extending one. A custom exception can carry extra data:

PHP
<?php
declare(strict_types=1);

final class InsufficientStockException extends RuntimeException
{
    public function __construct(
        public readonly string $sku,
        public readonly int $requested,
        public readonly int $available,
    ) {
        parent::__construct("Only $available left of $sku, but $requested were requested.");
    }
}

function reserve(array &$stock, string $sku, int $qty): void
{
    if ($qty <= 0) {
        throw new InvalidArgumentException('Quantity must be at least 1.');
    }
    $available = $stock[$sku] ?? throw new OutOfBoundsException("Unknown product: $sku");

    if ($qty > $available) {
        throw new InsufficientStockException($sku, $qty, $available);
    }
    $stock[$sku] = $available - $qty;
}

$stock = ['TEA-250' => 5, 'PEN-10' => 40];
$orders = [['TEA-250', 2], ['TEA-250', 9], ['SOAP-1', 1], ['PEN-10', 0]];

foreach ($orders as [$sku, $qty]) {
    try {
        reserve($stock, $sku, $qty);
        echo "Reserved $qty x $sku\n";
    } catch (InsufficientStockException $e) {
        echo 'Stock problem: ' . $e->getMessage() . ' (short by ' . ($e->requested - $e->available) . ")\n";
    } catch (InvalidArgumentException | OutOfBoundsException $e) {
        echo 'Bad order: ' . $e->getMessage() . "\n";
    }
}
Output
Reserved 2 x TEA-250
Stock problem: Only 3 left of TEA-250, but 9 were requested. (short by 6)
Bad order: Unknown product: SOAP-1
Bad order: Quantity must be at least 1.

Three modern features appear here:

  • Several catch blocks are checked from top to bottom, and the first matching one runs. Put more specific classes first.
  • A | B in one catch handles two exception types the same way.
  • throw as an expression (PHP 8.0) lets you write $x ?? throw new ... to throw when a value is missing.

You can also leave out the variable when you do not need it: catch (JsonException) { ... }.

Wrapping exceptions with previous

Low-level errors, like a JSON syntax error, mean little to the rest of the app. Catch them and throw a clearer exception, passing the original as previous so no detail is lost:

PHP
<?php
declare(strict_types=1);

final class SettingsException extends RuntimeException
{
}

function loadSettings(string $path): array
{
    if (!is_readable($path)) {
        throw new SettingsException("Cannot read $path");
    }

    try {
        return json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new SettingsException("Settings file $path is not valid JSON", previous: $e);
    }
}

try {
    loadSettings(__DIR__ . '/settings.json');   // contains {"theme": "dark",}
} catch (SettingsException $e) {
    echo $e->getMessage() . "\n";
    echo 'Caused by: ' . $e->getPrevious()?->getMessage() . "\n";
}
Output
Settings file /var/www/app/settings.json is not valid JSON
Caused by: Syntax error

Code that calls loadSettings() only needs to know about SettingsException, while a developer reading the log can still see the original cause.

Warnings are not exceptions

Many older PHP functions do not throw. They show a warning and return false. For example, file_get_contents() on a missing file warns Failed to open stream and returns false, and a try block will not catch that. Check return values of such functions, as in Reading & Writing Files. Newer APIs usually offer an exception mode: JSON_THROW_ON_ERROR for JSON and PDO::ERRMODE_EXCEPTION for databases.

Avoid the @ operator (as in @file_get_contents(...)), which hides warnings. Hidden problems are the hardest to debug.

Errors in development and in production

During development you want to see every problem. On a live site, error messages must never reach visitors, because they reveal file paths, SQL queries and sometimes passwords. Control this in php.ini:

SettingDevelopmentProduction
error_reportingE_ALLE_ALL
display_errorsOnOff
log_errorsOnOn
error_logoptionala file outside the web root

Then add a last line of defence for anything nobody caught. set_exception_handler() registers a function that runs for any uncaught exception or error:

PHP
<?php
declare(strict_types=1);

set_exception_handler(function (Throwable $e): void {
    error_log($e::class . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());

    http_response_code(500);
    echo 'Sorry, something went wrong. Please try again later.';
});

The visitor sees a polite message, and the full details go to the error log, where you can read them.

Common mistakes

  • Catching everything and ignoring it. An empty catch (Exception $e) {} hides bugs. At least log the problem.
  • Catching too early. Only catch where you can actually do something useful; otherwise let the exception travel up.
  • Using exceptions for normal flow. "No search results" is not an exception; return an empty array.
  • Leaving display_errors on in production. Visitors and attackers see your internals.

Key points

  • try runs risky code, catch handles a matching exception, and finally always runs.
  • Throwable splits into Error (bugs in code) and Exception (problems to expect and handle).
  • throw your own exceptions; extend built-in classes to add meaning and data.
  • Order catch blocks from specific to general; use A | B to share one handler.
  • Wrap low-level exceptions with previous: to keep the original cause.
  • In production, set display_errors=Off, log everything, and show visitors a friendly message.

Exercise

Write a function parseAge(string $input): int for a sign-up form. Throw an InvalidArgumentException with a clear message if the input is not a whole number, and a RangeException if the age is below 13 or above 120. Test it with '21', 'abc' and '9' in a loop that catches both exception types in one catch block and prints the messages.

Show solution
PHP
<?php
declare(strict_types=1);

function parseAge(string $input): int
{
    $age = filter_var(trim($input), FILTER_VALIDATE_INT);
    if ($age === false) {
        throw new InvalidArgumentException("\"$input\" is not a whole number.");
    }
    if ($age < 13 || $age > 120) {
        throw new RangeException('Age must be between 13 and 120.');
    }
    return $age;
}

foreach (['21', 'abc', '9'] as $input) {
    try {
        echo 'Age accepted: ' . parseAge($input) . "\n";
    } catch (InvalidArgumentException | RangeException $e) {
        echo 'Error: ' . $e->getMessage() . "\n";
    }
}
Output
Age accepted: 21
Error: "abc" is not a whole number.
Error: Age must be between 13 and 120.

filter_var() with FILTER_VALIDATE_INT returns false for text that is not a whole number, and the two exception types share one catch with |.

Quick Quiz

1. When does a finally block run?

2. Which statement catches an Error such as TypeError?

3. What should a live website do with an unexpected database exception?

4. Does try { $x = file_get_contents('missing.txt'); } catch (Exception $e) {} catch the missing file?

Learn PHP with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call