01 · THE IDEAREAD → TRY → LEARN
Why cleanup needs a guarantee
Code often has to undo something no matter how it ends: release a lock, close a handle, or record that a step finished. If you put that cleanup at the end of a try block, it is skipped whenever an exception jumps out early. If you copy it into every catch block as well, you will eventually forget one. A finally block solves this by running after the try and any matching catch, whatever happened inside them.
In this lesson the "resources" are plain log entries in an array. That keeps everything deterministic while teaching the same control flow you would use with real resources.
The basic shape
A finally block can follow catch blocks, or it can be used with try alone. It runs when the try finishes normally, when a catch handles an error, and when an exception is not caught at all.
function describe(int $n): string {
$log = [];
try {
$log[] = 'try';
if ($n < 0) {
throw new InvalidArgumentException('negative');
}
$log[] = 'ok';
} catch (InvalidArgumentException $e) {
$log[] = 'catch';
} finally {
$log[] = 'finally';
}
return implode(',', $log);
}
echo describe(1), "\n", describe(-1), "\n";try,ok,finally try,catch,finally
Both calls end with finally. Only the path in between differs.
finally and return
When a try block executes return, PHP evaluates the return value right away. It then runs finally, and only afterwards hands the value to the caller. So cleanup still happens, and later changes made in finally do not alter a value that was already computed.
function demo(array &$log): string {
try {
$log[] = 'return try';
return 'value';
} finally {
$log[] = 'cleanup';
}
}
$log = [];
$result = demo($log);
echo json_encode([$result, $log]), "\n";["value",["return try","cleanup"]]
Be careful with a return inside finally itself. It replaces the earlier return value, and it can also hide an exception that was on its way out. Most code should keep finally for cleanup only.
Unwinding through several levels
When an exception is not caught nearby, PHP unwinds the call stack. Every finally block on the way runs, from the innermost outward, before an outer catch handles the exception.
function nested(array &$log): void {
try {
try {
throw new RuntimeException('x');
} finally {
$log[] = 'inner';
}
} finally {
$log[] = 'outer';
}
}
$log = [];
try {
nested($log);
} catch (RuntimeException $e) {
$log[] = 'caught:' . $e->getMessage();
}
echo json_encode($log), "\n";["inner","outer","caught:x"]
The order is inner, outer, then caught:x. The cleanup runs before the handler.
Limits to remember
- If a script ends abruptly with
exit()or a fatal error,finallyblocks are skipped. - If both
tryandfinallythrow, the exception fromfinallyis the one that propagates, and the earlier one becomes its previous exception. - A
finallythat throws or returns can mask what happened before it, so keep it simple.
Worked plan for the exercise
You will write runSteps(array $steps): array. Each element is a step name. For every step, in order:
- Append
open:NAMEto a log. - Inside
try, if the name starts withbad, throw aRuntimeException. Otherwise appendwork:NAME. - In
catch, appenderror:NAME. - In
finally, appendclose:NAME.
Return the full log. A failing step must not stop later steps, and every opened step must be closed exactly once. For ["a", "bad"] the result is open:a, work:a, close:a, open:bad, error:bad, close:bad.
The tests check the returned array, not printed text. Cover an empty list, a good step, a failing step, and a mix. A solution that writes close only after the work line will fail the failing-step case. That is the exact bug finally prevents.
Recap
Use finally for actions that must happen on every exit path. It runs after try and catch, even with return or an uncaught exception. Nested blocks run inner to outer. exit() and fatal errors skip them, and returning or throwing from inside finally can hide earlier outcomes.
Functions · Arrays · Types
- Track deterministic in-memory cleanup without external resources
- Implement and test a deterministic PHP 8.4 function