Skip to content
Educora
Intermediate17 min8 / 10

Exceptions and error handling

Catch errors with try/catch/finally, understand the Exception and Error hierarchy, create your own exception class and set up error reporting correctly for development and production.

Check yourself
In this lesson you will learn
  • Throw and catch exceptions with throw, try, catch and finally
  • Explain the Throwable, Exception and Error hierarchy and write a custom exception
  • Configure error reporting correctly for development and production

The payment system does not respond, a user sends broken JSON, the database is unreachable — on a real website such things happen every day. A good program does not show a “white screen”: it catches the problem, gives the user a clear message and writes the details to a log. In PHP the main tool for this is exceptions.

try, catch and finally

When code detects a problem, it throws an exception with throw: the function stops at once, and the exception travels up the chain of calls until it finds a catch block that catches it. Risky code goes into a try block, catch catches a specific type of exception, and finally runs in every case — whether an exception happened or not.

PHP
<?php

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

foreach ([4, 0] as $b) {
    try {
        $result = divide(12, $b);
        echo "12 / $b = $result\n";
    } catch (InvalidArgumentException $e) {
        echo "Error: ", $e->getMessage(), "\n";
    } finally {
        echo "-- done with $b\n";
    }
}
Expected output
12 / 4 = 3
-- done with 4
Error: Cannot divide by zero
-- done with 0

When $b = 0, the echo line never ran: after throw, control jumped straight to catch. finally ran on both passes of the loop. Useful methods of an exception object: getMessage() — the message, getCode() — the code, getFile() and getLine() — where the error happened, getPrevious() — the previous exception.

Exception and Error

Since PHP 7 the common parent of everything that can be thrown is the Throwable interface. It has two branches: **Exception — problems in the program's own logic (a bad argument, a missing file, a failed database connection), and Error** — PHP's internal errors (a wrong type, division by zero, calling a method that does not exist).

BranchCommon classesWhen
ExceptionInvalidArgumentException, RuntimeException, JsonException, PDOExceptionexpected problems: catch and handle them
ErrorTypeError, ValueError, DivisionByZeroError, ArgumentCountError, UnhandledMatchErrorusually a bug in the code: fix it
PHP
<?php

$tests = [
    fn() => intdiv(10, 0),
    fn() => json_decode("{bad json", flags: JSON_THROW_ON_ERROR),
    fn() => throw new RuntimeException("Disk is full"),
];

foreach ($tests as $test) {
    try {
        $test();
    } catch (Throwable $e) {
        echo get_class($e), ": ", $e->getMessage(), "\n";
    }
}
Expected output
DivisionByZeroError: Division by zero
JsonException: Syntax error
RuntimeException: Disk is full

Your own exceptions

To create your own exception class, extend an existing one, usually RuntimeException or DomainException. Let the name describe the problem clearly and, if needed, keep extra data in a property. Then a catch block can react precisely to exactly this situation.

PHP
<?php

class InsufficientFundsException extends RuntimeException
{
    public function __construct(public readonly float $missing)
    {
        parent::__construct("Not enough money: missing $missing AZN");
    }
}

class Wallet
{
    public function __construct(private float $balance) {}

    public function pay(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($amount - $this->balance);
        }
        $this->balance -= $amount;
        echo "Paid $amount, left {$this->balance}\n";
    }
}

$wallet = new Wallet(30);
try {
    $wallet->pay(12);
    $wallet->pay(25.5);
    $wallet->pay(1);
} catch (InsufficientFundsException $e) {
    echo $e->getMessage(), "\n";
    echo "Top up at least ", ceil($e->missing), " AZN\n";
}
Expected output
Paid 12, left 18
Not enough money: missing 7.5 AZN
Top up at least 8 AZN
Old: checking the return value
$data = json_decode($input);
if ($data === null) {
    // broken JSON or the value null?
    echo json_last_error_msg();
}
Modern: an exception
try {
    $data = json_decode($input, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo "Invalid JSON: ", $e->getMessage();
}
null can mean either an error or a valid JSON value (null). An exception reports the error unambiguously.

Warnings and error reporting

Some problems do not throw an exception but just give a warning: for example, a missing variable produces Warning: Undefined variable $x, and an outdated function produces a Deprecated message. Which messages appear is decided by php.ini settings. The golden rule: during development see everything; in production show the user nothing and write everything to the log.

Setting (php.ini)DevelopmentProduction
error_reportingE_ALLE_ALL
display_errorsOnOff
log_errorsOnOn
You can also switch it on in code: error_reporting(E_ALL); ini_set('display_errors', '1');

Key points

  • throw throws an exception; try holds risky code, catch catches a specific type and finally always runs.
  • Throwable has two branches: Exception (expected problems) and Error (PHP's internal errors).
  • Extend an existing class for your own exception and give it a meaningful name; keep extra data in a property.
  • In PHP 8 throw is an expression: $x = $arr[$k] ?? throw new ...;
  • Use display_errors = On in development, and Off together with log_errors = On in production.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
When does the finally block run?