Lesson 88 of 100
Errors15 min5 checksPHP 8.5

Guarantee Cleanup with finally

Learn how a finally block runs after try and catch, even with return or uncaught exceptions, and use it to track deterministic cleanup.

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.

Example
<?php
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";
Output
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.

Example
<?php
function demo(array &$log): string {
    try {
        $log[] = 'return try';
        return 'value';
    } finally {
        $log[] = 'cleanup';
    }
}

$log = [];
$result = demo($log);
echo json_encode([$result, $log]), "\n";
Output
["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.

Example
<?php
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";
Output
["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, finally blocks are skipped.
  • If both try and finally throw, the exception from finally is the one that propagates, and the earlier one becomes its previous exception.
  • A finally that 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:

  1. Append open:NAME to a log.
  2. Inside try, if the name starts with bad, throw a RuntimeException. Otherwise append work:NAME.
  3. In catch, append error:NAME.
  4. In finally, append close: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.

Before you start

Functions · Arrays · Types

You’ll practice
  • Track deterministic in-memory cleanup without external resources
  • Implement and test a deterministic PHP 8.4 function
Your task

Make runSteps() 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.