Lesson 19 of 100
Errors12 min5 checksPHP 8.5

Handle Errors Explicitly with Exceptions

Learn to throw and catch typed exceptions in PHP so invalid input stays separate from valid results and failures are handled deliberately.

01 · THE IDEAREAD → TRY → LEARN

Why exceptions exist

A function sometimes receives input it cannot work with. You could return false, -1 or an empty string, but then every caller has to remember that special value and check for it. If one caller forgets, the bad value flows onward as if it were real data.

An exception solves this by interrupting normal flow. When you use throw, the function stops immediately. PHP then unwinds the call stack until it finds a matching catch block. Valid results are returned normally, and failures travel through a separate channel that cannot be mistaken for data.

The three pieces

  • throw creates the failure. It needs an object that is an Exception or Error (or a subclass of one of them).
  • try wraps the code that might fail.
  • catch (ClassName $e) handles a matching failure. $e->getMessage() returns the text you supplied.

Here is a first worked example. Notice that the second call never prints anything, because the exception leaves the try block before echo runs. Execution then resumes after the catch block.

Example
<?php
function parsePercent(int $value): int
{
    if ($value < 0 || $value > 100) {
        throw new InvalidArgumentException("Percent must be 0-100");
    }
    return $value;
}

try {
    echo parsePercent(40), "\n";
    echo parsePercent(140), "\n";
} catch (InvalidArgumentException $e) {
    echo "Caught: ", $e->getMessage(), "\n";
}
echo "Done\n";
Output
40
Caught: Percent must be 0-100
Done

Choose a meaningful type

PHP ships with many exception classes. InvalidArgumentException fits a bad argument value, and OutOfBoundsException fits a missing key. You can also define your own subclass when you want callers to react to one specific problem.

Catch blocks are checked from top to bottom, and the first one that matches the thrown class handles it. A catch for a parent class also catches its children. So place narrow classes before broad ones.

A finally block runs whether or not an exception happened, which makes it useful for cleanup or for always recording that a step finished. The next example uses a custom subclass and a loop to show both paths.

Example
<?php
class EmptyNameException extends InvalidArgumentException {}

function greet(string $name): string
{
    $name = trim($name);
    if ($name === '') {
        throw new EmptyNameException('Name is required');
    }
    return 'Hello, ' . $name;
}

foreach (['Ada', '  '] as $input) {
    try {
        echo greet($input), "\n";
    } catch (EmptyNameException $e) {
        echo 'Invalid: ', $e->getMessage(), "\n";
    } finally {
        echo "Checked\n";
    }
}
Output
Hello, Ada
Checked
Invalid: Name is required
Checked

throw is an expression

Since PHP 8.0, throw can appear anywhere an expression is allowed. That makes compact guards possible, for example on the right side of the null coalescing operator ??. If the key exists you get its value; otherwise the exception is thrown.

Example
<?php
function requireKey(array $data, string $key): string
{
    return $data[$key] ?? throw new OutOfBoundsException("Missing key: $key");
}

$config = ['host' => 'localhost'];
echo requireKey($config, 'host'), "\n";
try {
    echo requireKey($config, 'port'), "\n";
} catch (OutOfBoundsException $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}
Output
localhost
OutOfBoundsException: Missing key: port

What not to do

  • Do not leave an exception uncaught unless you want the script to stop. An uncaught exception ends in a fatal error, unless a handler registered with set_exception_handler() deals with it.
  • Do not catch Error objects to hide bugs. They usually signal a programming mistake that should be fixed, not silenced.
  • Do not throw an exception inside a catch block and expect the next sibling catch to handle it. Sibling blocks only see exceptions from the try body.
  • Do not use exceptions for ordinary, expected branching such as "item not found in a search" when a plain return value is clearer.

Your exercise

Write safeIntDivide(int $a, int $b): array. It should use a helper that throws InvalidArgumentException with the message Division by zero when $b is 0, and otherwise returns intdiv($a, $b). The public function catches that exception and always returns an array:

  • on success: ['ok' => true, 'value' => <quotient>]
  • on failure: ['ok' => false, 'error' => <message>]

The caller never sees an exception, and a valid quotient of 0 is clearly different from a failure because the ok flag says which one happened. Check the boundaries: a zero numerator, a negative numerator, and a zero divisor.

Recap

Throw when a function cannot honestly produce a result, catch where you know how to respond, and choose a specific exception class so the handler can be precise. Keep success and failure on separate paths.

Before you start

Functions · Conditionals

You’ll practice
  • Throw and catch a typed exception
  • Keep invalid inputs separate from valid results
Your task

Make safeIntDivide() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.