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:
- The
tryblock reached its end—finallyruns next. - A
returnran intryorcatch—the value is set aside,finallyruns, and only then does the function return the value it set aside. - An exception was thrown—
finallyruns after the matchingcatch, 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.
1<?php2 3function parse(string $s): int4{5 try {6 if ($s === '') {7 throw new Exception('empty');8 }9 return (int) $s;10 } catch (Exception $e) {11 echo 'caught', "\n";12 return 0;13 } finally {14 echo 'finally', "\n";15 }16}17 18echo parse('42'), "\n";19echo parse(''), "\n";
To print the result, PHP calls parse('42').
The program printed finally, 42, caught, finally, 0. In both calls, finally ran before echo got the result:
- For
'42',returnset42aside,finallyprinted its line, and only then didechoprint the number. - For the empty string,
throwcuttryshort,catchprintedcaughtand set0aside, and thenfinallyran 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
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:
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:
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
class OutOfStockException {}
throw new OutOfStockException('only 5 left'); // ✗Fatal error: Uncaught Error: Cannot throw objects that do not implement ThrowableOnly 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
finallyruns on any way out oftry: after the end of the block, afterreturnand after an exception.- A value from
returnwaits untilfinallyis over, and only then goes to the calling code. - A custom exception class extends
Exceptionor one of its descendants; it gets the message and the code from the parent. - A
catchfor a parent class catches the descendants too,get_class($e)names the real class, andthrow $epasses the exception on.