Finally Getting finally In PHP?

It’s quite possible that PHP may finally be getting the addition of the finally block in its try/catch block. This would mean PHP may be inline with supporting finally, as many other languages, with a similar exception model, already do. This has been a requested feature in PHP dating as far back as early 2005 (as reported in the bug tracker). It also solves a simple, but overlooked problem for the developer. With finally we offer the user-space code a chance to do any clean up work that may be necessary after a try block has terminated execution and with clear semantics. This is actually quite reasonable as an expectation for a developer, because there are many situations wherein exception handling can get rather tricky. It’s nice that this feature makes code both more readable as well as easier to write/debug. This new feature is still in the RFC stages, but so far the vote is swinging in favor of adding it to PHP. Personally, I have followed the discussion for this feature from the beginning and I’m seeing some good arguments in favor of, and very few good arguments against, adding finally to PHP. I’m for the change since it introduces a reasonable solution to a common problem without burdening the developer in having to test/refactor code with exception handling where clean up work becomes mandatory.

You can try the current patch here from the original author’s (laruence) github repository. I’ve very much enjoyed testing this patch and seeing some immediate benefits in existing code.

A Good Use Case

To demonstrate, lets take a look at a good use case for finally. First, lets assume we a have a function that handles some mundane task like adding a record to the database. Now, lets assume that if adding this record fails, that this will also require writing to a log file whereby an entry is made in case of some set of conditions. Then, we will introduce an Exception that will be thrown by the function in the event something happens to go wrong when writing to the log file. However, keep in mind that the function also has to handle exceptions thrown by the database object in the event something goes wrong with the database operation inside of the function. These will be caught from the local scope and handled therein. Any other exceptions thrown inside the function that aren’t related to the database should be handled by the caller (i.e. they will get caught higher up the call chain). So to make this simple example self-contained I’ve introduced two dummy (or mock) objects (the DB object and the Record object) as well as the two functions we’ll be using to add a record to the database and write to the log. Here is the code for the mock classes and functions.

class DB {
  public function lock() {}
  public function unlock() {}
  public function prepare() {}
  public function exec() { throw new Exception('Transaction failed!'); }
  public function rollback() {}
}

class Record {
  public $sql = "INSERT INTO `table` (purchase_id,customer_id,product_id) VALUES(?, ?, ?);";
  public $table = "table";
  public $params = array(1,1,1);
}

function write_log($message, $trace, $test = false) {
  if (!$test) return false;
  return true;
}

function addRecord($record, $db) {
  try {
    $db->lock($record->table);
    $db->prepare($record->sql);
    $db->exec($record->params);
  }
  catch(Exception $e) {
    if (!write_log($e->getMessage(), $e->getTraceAsString())) {
      throw new Exception('Unable to write to error log.');
    }
    $db->rollback($record);
    /* At this point we're going to want to cleanup */
    $db->unlock($record->table);
    /* Now we can return false */
    return false;
  }
  /* finally when we're done */
  $db->unlock($record->table);
  return true;
}

As you can see these mock objects don’t actually do anything and are designed specifically to throw an exception to simulate a test case. Here’s what would happen if we tried adding a record to the database where the transaction fails. The assumption here is that the database table or row needs to be locked first before we can proceed with the transaction. The other assumption is that if the database table is not later unlocked after we’re done with adding the record that this will result in a failed test.

$db = new DB; /* Our dummy DB Object */
$record = new Record; /* Our dummy Record Object */

try {
  /* Call our mock addRecord function for testing */
  $result = addRecord($record, $db);
}
catch(Exception $e) {
  /* Handle the exception here */
  echo "Unable to add the record. Got Exception with message: '{$e->getMessage()}'\n";
}

/* Test the function's return value here */
var_dump($result);

OUTPUTWhen write_log()’s third argument is false

Unable to add the record. Got Exception with message: 'Unable to write to error log.'
NULL

OUTPUTWhen write_log()’s third argument is true

bool(false)

OUTPUTWhen no exceptions are thrown

bool(true)

Now, the obvious problem here is that we always need to make sure the database object and its related procedures are handled inside the callee as opposed to being handled by the caller. This makes sense because the caller has other things to worry about like handling other exceptions thrown by the callee. Allowing the cleanup to be handled higher up in the call stack causes an extra burden of cleanup work that may result in further difficulty during testing/debugging and can even lead to some hard-to-track bugs once the stack becomes complex enough. Handling it after the try/catch blocks, or even inside of the catch block, while entirely possible, is not making for easy to read code and as you can see from our example here it has a side-effect of causing code duplication.

In the example above the first test (using write_log() without a third argument) causes an Exception to be thrown from inside of the function addRecord’s catch block. This means we must remember to throw the exception only after cleanup is done. That’s a problem here, because we could accidentally overlook this part as was overlooked here. So neither $db->rollback() nor $db->unlock() will get called in the first example and the function implicitly returns null. We could also use nested try/catch inside of the first catch block. However, this still leaves us with the burden of code duplication.

If the catch block should happen to return from the function (as in the case of a failure here we will return false), there’s no chance to do the cleanup work anywhere else. So we either have to do it inside of the catch block, like we did here, and return false from there or let the catchblock fall through and then test for a conditional later on in order to do cleanup work and then return. However, this also means that we’ve distinctly separated the code that handles cleanup from the code that handles the exception (thereby decoupling the try/catch block from the cleanup). This creates some disambiguation for the reader and that’s where we get the code readability problem. So that’s how we could do it today, in PHP, without finally… Well, it works, but it isn’t always obvious when we read this code what should happen, which means adding-to/modifying the code requires further thought about execution flow and how it affects the caller.

We can make it a little easier and also a lot more logical if we just change our way of thinking about how we handle Exceptions. When I say “we” I really mean “we” as in the current users of PHP since we’ve never had finally before and managed to do without.

Introducing finally!

So here’s where finally comes in. The finally block always executes in the presence of a try block. This means regardless of whether or not the catch block gets executed the finally block should keep its promise in executing its instructions.

function addRecord($record, $db) {
  try {
    $db->lock($record->table);
    $db->prepare($record->sql);
    $db->exec($record->params);
  }
  catch(Exception $e) {
    $db->rollback($record);
    if (!write_log($e->getMessage(), $e->getTraceAsString())) {
      throw new Exception('Unable to write to error log.');
    }
  }
  finally {
    $db->unlock($record->table);
  }
  return true;
}

finally execution flow
Basically the execution flow of finally can be outlined by the diagram to the right here. If the finally keyword is used where a try block is present, the try/catch blocks work normally, until the we get the end of the execution phase for the entire block, whereby finally is then forced in. So whether an Exception is thrown or even caught, finally comes in as part of the condition that try is present.

Now, we can always rest assured that the code inside of the finally block will always run despite whatever flow of control the rest of the code inside of the try/catch blocks may be. So now we have a nicer solution and it allows our code to look better, behave smarter, and makes it easier for us to debug these types of issues where cleanup is important. However, before you go off getting too excited with finally, here are a few pointers you should keep in mind about how you shouldn’t use finally. There are at least a few simple gotchyas that I’ve come across so far.

A Few Important Notes

First, consider that any return statement as a part of your try or catch blocks will be delayed until the finally block is executed (if it’s present). Additionally, if a return statement is present in your finally block it overrides the previous return. Lets illustrate with the following two examples…

function test($var = false) {
  try {
    if (!$var) throw new Exception;
    return 1;
  }
  catch(Exception $e) {
    return 2;
  }
  finally {
    return 3;
  }
}

var_dump(test(), test(1));

OUTPUT

int(3)
int(3)

In both cases the function ends up returning 3 because finally gets the last say (and thus its return statement supersedes any previously delayed return statement). Now, if you happen to return inside of your try or catch block, finally still executes before the function returns (remember that’s a part of the promise finally makes) as in both cases below…

function test($var = false) {
  try {
    echo "try real hard...\n";
    if (!$var) throw new Exception;
    return 1;
  }
  catch(Exception $e) {
    echo "catch what you can\n";
    return 2;
  }
  finally {
    echo "and finally! go home :)\n";
  }
}

var_dump(test(), test(1));

OUTPUT

try real hard...
catch what you can
and finally! go home :)
try real hard...
and finally! go home :)
int(2)
int(1)

What’s also important to remember is that finally executes whether an exception has been thrown or not. So while it’s possible to use a try/finally block to wrap up code that requires cleanup where the try-block might introduce some flow of control that is only made clear at run time, it’s not advisable to depend on this behavior. Simple because in the following example if we were to call a function that calls die or exit, finally would still never get executed.

function test() {
  if (!func_num_args()) die("ohnoes!\n");
}

try {
  test(1);
  test();
}
finally {
  echo "If something should go wrong above... we might not end up here!\n";
}

OUTPUT

ohnoes!

This is already an issue discussed in the initial proposal and it seems even in other languages, like Java, finally does not guarantee execution in the event of an explicit shutdown or fatal error.

If all goes well we could finally see finally in the next release of PHP :)

5 Responses to“Finally Getting finally In PHP?”

  1. Markus
    August 8, 2012 at 8:18 am #

    As far as I’ve heard from people on internals, that’s not really something that is seriously discussed.

  2. August 11, 2012 at 4:27 am #

    GoogleGuy, thanks for posting this.

    @Markus we discussed it in IRC :)

  3. GoogleGuy
    August 28, 2012 at 2:09 pm #

    * Updated: The RFC has been accepted into the source tree after a majority vote win. I updated the link in the article to the actual commit after merging.

    Cheers! :)

Pingbacks/Trackbacks

  1. PHP????finally | ???? - August 16, 2012

    [...] ?????????finally?????(??: Finally Getting finally In PHP?), ??????????????: [...]

  2. php-build ? PHP 5.5 ???????? - Born Too Late - September 25, 2012

    [...] ????? 5.5snapshot ?????????????? ??? Generator ? finally ?????? ??????? World Domination [...]

Leave a Reply

Your email address will not be published. Required fields are marked *

(Required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>