Skip to content
Theory · Errors and exceptions · lesson 6 of 8

finally and custom exceptions

The courier keeps a round log. Whatever happens at a door—the parcel is delivered, a note is left, or the parcel goes back—the courier signs the log. Without the signature, nobody knows at which flat the round ended.

Code also has actions that are needed whatever the outcome: close a file, release a resource, write "done". If you put such a line after try and catch, a return inside try skips it, and so does an exception that nobody catches. For this, try has a third block, finally.

The finally block

The finally block goes after try and the catch blocks, and catch can be left out altogether. PHP runs finally on any way out of try:

  1. The try block reached its end—finally runs next.
  2. A return ran in try or catch—the value is set aside, finally runs, and only then does the function return the value it set aside.
  3. An exception was thrown—finally runs after the matching catch, and if there is none, before the exception goes further up.

Let's follow a function that leaves try in two ways: through return and through throw. Press "Next step" and watch at which step finally prints and when echo gets the result.

Livefinally after return and after catchstep 1 of 12 · frames: 1

To print the result, PHP calls parse('42').

Watch when finally prints and when echo gets the result

The program printed finally, 42, caught, finally, 0. In both calls, finally ran before echo got the result:

  1. For '42', return set 42 aside, finally printed its line, and only then did echo print the number.
  2. For the empty string, throw cut try short, catch printed caught and set 0 aside, and then finally ran again.

Interviewers often ask whether finally runs when try has a return. Yes: return sets the value aside, finally runs, and only then does the function return it. A true fatal error, such as running out of memory, can skip finally, because it stops the program at once.

Here the log picture stops working. The courier signs after the delivery, while finally runs before the value from return reaches the calling code.

Custom exception classes

Failures are hard to tell apart by message text: the text changes, and catch does not read it. It is more reliable to give a failure its own class. A custom exception class extends a ready-made one, and an empty body is enough: the constructor with a message and a code, getMessage and getCode come from the parent.

PHP
<?php

class OutOfStockException extends RuntimeException {}

function reserve(int $stock, int $wanted): int
{
    if ($wanted > $stock) {
        throw new OutOfStockException("only $stock left", 42);
    }
    return $stock - $wanted;
}

Such an exception can be caught by its own class or by any parent:

PHP
try {
    echo reserve(5, 2), "\n"; // 3
    echo reserve(5, 9), "\n";
} catch (RuntimeException $e) {
    echo get_class($e), "\n"; // OutOfStockException
    echo $e->getCode(), ' ', $e->getMessage(), "\n"; // 42 only 5 left
}

OutOfStockException is a descendant of RuntimeException, so catch (RuntimeException $e) catches it. The object itself does not change: get_class($e) names the real class, and the message and the code 42 are the ones passed to new.

Passing it on: throw $e

Sometimes a function can note a failure but cannot fix it. Then in catch it records what happened and throws the same object again, with throw $e:

PHP
function buy(int $wanted): int
{
    try {
        return reserve(5, $wanted);
    } catch (OutOfStockException $e) {
        echo 'log: ', $e->getMessage(), "\n"; // log: only 5 left
        throw $e;
    }
}

try {
    buy(9);
} catch (RuntimeException $e) {
    echo 'Sorry, sold out', "\n"; // Sorry, sold out
}

buy printed a line starting with log:, as if writing to a log, and sent the exception further up. The outer catch caught it and showed the customer a clear message.

Common mistake

A custom exception class is declared without extends:

PHP
<?php

class OutOfStockException {}

throw new OutOfStockException('only 5 left'); // ✗
TEXT
Fatal error: Uncaught Error: Cannot throw objects that do not implement Throwable

Only an object that implements Throwable can be thrown, and a plain class does not implement it. Add a parent from the Exception branch: class OutOfStockException extends RuntimeException {}.

Key points

  • finally runs on any way out of try: after the end of the block, after return and after an exception.
  • A value from return waits until finally is over, and only then goes to the calling code.
  • A custom exception class extends Exception or one of its descendants; it gets the message and the code from the parent.
  • A catch for a parent class catches the descendants too, get_class($e) names the real class, and throw $e passes the exception on.