Blog: or die() must die

Views:
98209

This is something I constantly see on the PHP Freaks forums [http://www.phpfreaks.com/forums/], and I absolutely hate it. It's the infamous or die() statement.

$result = mysql_query('SELECT foo FROM bar', $db) or die('Query failed: ' . mysql_error($db));

I see it all the time, and I see people telling other people to do that all the time. It's plain simply bad practice and it's time that people start to understand this. When I confront people with it they usually say something along the lines of "oh, but it's just for debugging purposes". Okay, so I tend to put echo and var_dump() statements in my code for debugging as well. However, this is not the same. It's quite obvious that you wouldn't want an array var_dump'ed to the screen in the final application, but you certainly do want some sort of error handling for when e.g. database queries fail.

The or die() trick is a very poor choice for several reasons:

  1. It's not a very nice way to present the user with an error message.
  2. Using for instance the mysql_error() call with it, as many people do, exposes information that should never get output in a production environment (see: PHP Security [http://www.phpfreaks.com/tutorial/php-security])
  3. You cannot catch the error in any way.
  4. You cannot log the error.
  5. You cannot control whether it should be output to the screen or not. It's okay to do that in a development environment, but certainly not in a production environment.
  6. It prevents you from doing any sort of cleanup. It just ends the script abruptly.

So if your little or die() trick is just for debugging purposes, how are you going to handle potential errors after you've deployed your application? Fortunately, PHP does actually include a function that allows you to raise PHP errors on runtime: trigger_error() [http://php.net/trigger_error]

This function allows you to raise errors of type E_USER_NOTICE, E_USER_WARNING and E_USER_ERROR. These behave exactly like their non-USER counterparts, i.e. errors of type E_USER_ERROR are fatal and halts execution while the other two doesn't. If you are used to doing the or die() it's also very easy to implement:

$result = mysql_query('SELECT foo FROM bar', $db) or trigger_error('Query failed: ' . mysql_error($db), E_USER_ERROR);

Syntactically this is very much like the previous code snippet, but much better. Because errors of these types behave like errors PHP would normally make you can also use all the facilities PHP has for error handling [http://php.net/errorfunc]. You can implement a custom error handler [http://php.net/set_error_handler] so you can display nice messages to your user, and you can log the errors to a file [http://php.net/manual/en/errorfunc.configuration.php#ini.error-log]. Finally you can disable output of errors [http://php.net/manual/en/errorfunc.configuration.php#ini.display-errors] in a production environment.

Another option is to use exceptions [http://php.net/manual/en/language.exceptions.php]. Example:

if (!$result = mysql_query('SELECT foo FROM bar', $db)) {
	throw new Exception('You fail: ' . mysql_error($db));
}

Personally, I prefer exceptions because they're easily catchable using try-catch blocks, which makes it pretty easy to degrade if something unexpected, or rather an exception, something that doesn't behave as expected, occurs. As an added bonus you you will get a stack trace which aids in debugging.

A great example of exception usage could be database transactions using PDO [http://php.net/pdo] (MySQLi [http://php.net/mysqli] supports it as well if you prefer that). When you normally run a query on a database server it will be committed instantaneously. When you start a transaction changes won't be committed until you explicitly say so. This allows you to rollback if you wish.

We might have a setup like this:

+---------+------------+-----------+-------+
| user_id | first_name | last_name | funds |
+---------+------------+-----------+-------+
|       1 | Daniel     | Egeberg   |   500 |
|       2 | John       | Doe       |   350 |
+---------+------------+-----------+-------+

We want to transfer 120 from Daniel to John. This involves two operations: 1) Removing funds from Daniel, and 2) Adding funds to John. We need both or none of those operations to finish successfully. Executing just one of them is not an acceptable scenario.

Here is our transfer script:

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');

$transferAmount = 120;
$fromUserId = 1;
$toUserId = 2;

try {
	$db->beginTransaction();
	
	$stmt = $db->prepare('UPDATE users SET funds = funds + :amount WHERE user_id = :user_id');
	
	// remove funds from sender
	$stmt->execute(array('amount' => $transferAmount * -1, 'user_id' => $fromUserId));
	
	// add funds to recipient
	$stmt->execute(array('amount' => $transferAmount, 'user_id' => $toUserId));
	
	$db->commit(); // we will only get to this if both the above queries executed successfully
}
catch (PDOException $e) {
	$db->rollback(); // damn... well, at least we can roll back
	
	echo 'Sorry, but we were unable to transfer the funds. Please contact customer support if the problem persists.';
	// log this incident somehow. further info is available using $e->getMessage()
}

Finally just a quick check that we did in fact transfer the money:

+---------+------------+-----------+-------+
| user_id | first_name | last_name | funds |
+---------+------------+-----------+-------+
|       1 | Daniel     | Egeberg   |   380 |
|       2 | John       | Doe       |   470 |
+---------+------------+-----------+-------+

Yup. Everything's good!

In this case our good friend or die() is simply not an option. If the first statement ran fine, but the second failed I would have just lost money on nothing, and because the programmer was too lazy to implement real error handling nobody would be able to figure out what happened, and the case couldn't be documented. I certainly would not appreciate that...

People need to stop that or die() nonsense, and even more importantly, people need to stop teaching other people their own bad practice. Calling die() when something happens is not an acceptable way of handling the situation.