facebook twitter
Webucator's Free PHP Tutorial

Lesson: Exception Handling

Welcome to our free PHP tutorial. This tutorial is based on Webucator's Introduction to PHP Training course.

Like most programming languages, PHP throws exceptions (i.e., reports an error with detailed information) when something goes wrong. The programmer can anticipate, catch, and handle those exceptions in the code.

Lesson Goals

  • To catch and gracefully handle exceptions.
  • To log errors.

Uncaught Exceptions

If an exception is thrown by PHP and there is no code in place to handle the exception, then PHP will log the exception in the php_error.log and, if the display_errors directive is on, send the error to the browser to display. If the error is fatal, no further code will be processed.

The following example shows how PHP handles division by zero, which generates a warning, the lowest level type of error. This does not stop execution of the code.

Code Sample:

ExceptionHandling/Demos/division-by-zero.php
<?php
  ini_set('display_errors', '1');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../static/styles/normalize.css">
<link rel="stylesheet" href="../../static/styles/styles.css">
<title>Division by Zero</title>
</head>
<body>
<main class="pre">
<?php
  echo 'Error will be logged in ' . ini_get('error_log');
  echo '<hr>';

  $num = 5;
  $den = 0;
  $result = $num / $den;
  echo $result;
?>
</main>
</body>
</html>

Code Explanation

Visit http://localhost:8888/Webucator/php/ExceptionHandling/Demos/division-by-zero.php to run this file in your browser. You should see something like this:Uncaught Division by Zero

Notice that the code continues to run after the error is output. INF (for infinity) is the result of division by zero.

We have used ini_get('error_log') to find out the location of php_error.log: /Applications/MAMP/logs/php_error.log. The location may be different on your computer. Open that file in any text editor and you should see the error reported:

[29-Jan-2019 19:07:09 UTC] PHP Warning: Division by zero in /Applications/MAMP/htdocs/Webucator/php/ExceptionHandling/Demos/division-by-zero.php on line 21

You may want to keep php_error.log open for the remainder of this lesson as we will be referring to it again.

Throwing Your Own Exceptions

It is possible to throw your own exceptions using throw. While it's likely you won't have to do this unless you are creating PHP libraries that are used by other developers, it is worth seeing how it is done as you will certainly be catching exceptions thrown by libraries and PHP extensions that you use in your code.

In the following example, we create our own divide() function which throws an exception if the denominator is 0:

Code Sample:

ExceptionHandling/Demos/uncaught-exception.php
<?php
  ini_set('display_errors', '1');
  function divide($numerator, $denominator) {
    $numerator = (int) $numerator;
    $denominator = (int) $denominator;
    if ($denominator === 0) {
      throw new Exception('YOU CANNOT DIVIDE BY ZERO!');
    }
    return $numerator / $denominator;
  }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../static/styles/normalize.css">
<link rel="stylesheet" href="../../static/styles/styles.css">
<title>Uncaught Exception</title>
</head>
<body>
<main class="pre">
<?php
  echo 'Error will be logged in ' . ini_get('error_log');
  echo '<hr>';

  $num = 5;
  $den = 0;
  $result = divide($num, $den);
  echo $result;
?>
</main>
</body>
</html>

Code Explanation

Notice that we cast $numerator and $denominator as integers. We do this in case someone passes in a string (e.g., '0'), which could be the case if they are getting the number from a form submitted by a user.

Visit http://localhost:8888/Webucator/php/ExceptionHandling/Demos/uncaught-exception.php in your browser. You should see something like this:Uncaught Exception

Notice that the message we threw in our code is shown in the error report. The same error will be reported in php_error.log.

Also notice that the code does not continue to run after the error is output. Exceptions that are thrown this way are fatal. To prevent them from stopping the program, you have to catch them.

Catching Exceptions

Getting Information about Exceptions

Exception objects have methods for getting information about the exception. Three useful methods are:

  1. getMessage() - returns the exception's message.
  2. getFile() - returns the path to the file in which the exception occurred.
  3. getLine() - returns the line number on which the exception occurred.

To catch an exception, you have to anticipate when it might occur. We know that the divide() function can throw an exception, so we should be prepared for this possibility. The following code tries to pass divide() a value of 0 for the denominator and then catches the exception that divide() throws:

Code Sample:

ExceptionHandling/Demos/try-catch.php
<?php
  ini_set('display_errors', '1');
  function divide($numerator, $denominator) {
    $numerator = (int) $numerator;
    $denominator = (int) $denominator;
    if ($denominator === 0) {
      throw new Exception('YOU CANNOT DIVIDE BY ZERO!');
    }
    return $numerator / $denominator;
  }
?>
---- C O D E   O M I T T E D ----
<?php
  $num = 5;
  $den = 0;
  
  try {
    $result = divide($num, $den);
    echo $result;
  } catch (Exception $e) {
    echo '<h3>There was a problem:</h3>';
    $errorMsg = $e->getMessage();
    echo $errorMsg;
    error_log( $errorMsg . ' in ' . $e->getFile() . 
      ' on line ' . $e->getLine() );
    echo '<hr>';
    echo 'Error logged in ' . ini_get('error_log');
  }
?>
---- C O D E   O M I T T E D ----

Code Explanation

Visit http://localhost:8888/Webucator/php/ExceptionHandling/Demos/try-catch.php in your browser. You should see something like this:Try / Catch

Notice that the try / catch syntax is similar to the if / else syntax, but unlike if / else, in try / catch code, the catch block is required.

You will also see the error reported in php_error.log, but only because we explicitly logged it using the built-in error_log() function. This code:

error_log( $errorMsg . ' in ' . 
    $e->getFile() . on line ' . $e->getLine() );

... resulted in the following logged error:

[29-Jan-2019 19:12:21 UTC] YOU CANNOT DIVIDE BY ZERO! in /Applications/MAMP/htdocs/Webucator/php/ExceptionHandling/Demos/try-catch.php on line 7

Division Form

Duration: 10 to 15 minutes.

In this exercise, you will process data passed in through a form to either return the result of a division equation or an error message. The starting code is shown below:

Code Sample:

ExceptionHandling/Exercises/division.php
<?php
  ini_set('display_errors', '1');
  function divide($numerator, $denominator) {
    $numerator = (int) $numerator;
    $denominator = (int) $denominator;
    if ($denominator === 0) {
      throw new Exception('YOU CANNOT DIVIDE BY ZERO!');
    }
    return $numerator / $denominator;
  }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../static/styles/normalize.css">
<link rel="stylesheet" href="../../static/styles/styles.css">
<title>Division</title>
</head>
<body>
<main>
  <?php
    $num = $_GET['num'] ?? '';
    $den = $_GET['den'] ?? '';
    if (is_numeric($num) && is_numeric($den)) {
      // Your code here
    }
  ?>
  <form method="get" action="division.php">
    <label for="den">Numerator:</label>
    <input id="num" name="num" type="number" value="<?= $num ?>">
    <label for="den">Denominator:</label>
    <input id="den" name="den" type="number" value="<?= $den ?>">
    <button>DIVIDE</button>
  </form>
</main>
</body>
</html>

Code Explanation

Notice that we check if $num and $den are numeric. If they are not, it is likely because the form hasn't been submitted and they are both empty strings, so we won't try to do any division.

  1. Open ExceptionHandling/Exercises/division.php in your editor.
  2. Add a try / catch block:
    1. Attempt to divide using the numbers the user entered in the form. If all goes well, output something like:
      <output>5 / 2 = 2.5</output><hr>
      Division Success
    2. If the user enters 0 for the denominator, output an error similar to:
      <h3>Error!</h3>
      <p>You cannot divide by zero. Please try again.</p>
      Division Failure

Solution:

ExceptionHandling/Solutions/division.php
---- C O D E   O M I T T E D ----
  <?php
    $num = $_GET['num'] ?? '';
    $den = $_GET['den'] ?? '';
    if (is_numeric($num) && is_numeric($den)) {
      try {
        $result = divide($num, $den);
        echo '<output>' . $num . ' / ' . $den . 
          ' = ' . $result. '</output><hr>';
      } catch (Exception $e) {
        echo '<h3>Error!</h3>
          <p>You cannot divide by zero.</p>';
      }
    }
  ?>
---- C O D E   O M I T T E D ----

PDOExceptions

We have been using the PDO extension to connect to our database. Generally, extensions like this will have specific types of exceptions that are extended from PHP's built-in exceptions. When your PDO code errors, it will throw a PDOException. So, try / catch blocks will be structured like this:

try {
  // PDO code here
} catch (PDOException $e) {
  // Handle error
}

When working with external systems, like databases, email, the file system, etc., a lot can go wrong that is outside the PHP developer's control. For example, a database can go down, or the sign-in credentials can change, or table names can be changed. Because of these possibilities, you should write your code expecting things to go wrong, so your website doesn't just stop working without any explanation to the user.

Now you will have the chance to try this out on the Poetry website with a few exercises:

  1. First, you will do an exercise in which you create a logError() function in utilities.php that handles how errors are logged.
  2. Second, you will do an exercise in which you create a dbConnect() function in utilities.php that connects to the database and returns the connection. If there is an error, this function will log the error and return false.
  3. Third, you will modify the site's PHP files to make use of this new dbConnect() function and you will add code to properly handle errors when preparing and executing queries.

Logging Errors

Duration: 20 to 30 minutes.

In this exercise, you will create a logError() function in utilities.php that handles how errors are logged.

  1. Open ExceptionHandling/Exercises/phppoetry.com/includes/utilities.php in your editor.
  2. Add a function with the following signature:
    void logError(mixed $e, [bool $redirect=false])
    1. void means the function does not return anything.
    2. mixed means that $e could be more than one type.
    3. The $redirect parameter is an optional boolean value, which defaults to false.
  3. Create a $msg variable to hold the error message to be logged. Use the gettype() function and an if - else block or a switch / case block to check the type of $e. If the type of $e is a string, then $e should be assigned to $msg. Otherwise, the function should assume the type is an Exception object and it should construct the error message from $e->getMessage(), $e->getFile(), and $e->getLine() and assign the result to $msg.
  4. Log $msg to the error log.
  5. If in debug mode, output the following to the page:
    <h3 class='error'>For Developers' Eyes Only</h3>
    <div class='error'>$msg</div>
  6. If $redirect is true and not in debug mode, the function should redirect to an error page using the following line of code:
    header("Location: error-page.php");
    This tells the browser not to show the page with the error, but to instead redirect to error-page.php.
  7. Visit http://localhost:8888/Webucator/php/ExceptionHandling/Exercises/phppoetry.com/log-error-test.php to test your code. Enter any number for the numerator and 0 for the denominator and press the Divide button. You should see a result like this:Log Error Test 1If this doesn't work, fix your code and retest. If the page doesn't work at all, check php_error.log to see what went wrong.
    1. To see how the error would work on production, open utilities.php in your editor and change the isProduction() function to return true instead of false. Then run the file again. This time, you should get redirected to an error page:Log Error Test 2
    2. Be sure to change the isProduction() function back to returning false.

Solution:

The logError() function should look like this:

function logError($e, $redirect=false) { 
  $errorType = gettype($e);
  switch ($errorType) {
    case 'string':
      $msg = $e;
      break;
    default:
      $msg = $e->getMessage() . ' in ' . $e->getFile() . 
        ' on line ' . $e->getLine();
  }
  error_log($msg); // php_error.log

  if (isDebugMode()) {
    echo "<h3 class='error'>For Developers' Eyes Only</h3>
      <div class='error'>$msg</div>";
  }

  if ($redirect && !isDebugMode()) {
    // Redirect to error page
    header("Location: error-page.php");
  }
}

The dbConnect() Function

Duration: 20 to 30 minutes.

In this exercise, you will create a dbConnect() function in utilities.php that connects to the database and returns the connection. If there is an error, this function will log the error and return false. The function signature is as follows:

mixed dbConnect()

mixed means the function can return different types. Often, when a function can return different types, it returns the expected object if all goes well and returns false if something goes wrong. This is the case for the dbConnect() function you are about to create.

  1. Open ExceptionHandling/Exercises/phppoetry.com/includes/utilities.php in your editor.
  2. Add a line at the top to include config.php, which we placed in the includes directory outside of the web root earlier in the course. That file contains a getDbConfig() function, which returns an array with keys for 'dsn', 'un', and 'pw'.
  3. Create a new function called dbConnect() that:
    1. Sets $dbConfig to what getDbConfig() returns.
    2. Sets $dsn to $dbConfig['dsn'].
    3. Sets $username to $dbConfig['un'].
    4. Sets $password to $dbConfig['pw'].
    5. Attempts to create and return a database connection.
    6. On failure, it should return false after calling logError() and passing it the exception and true, meaning it should redirect to the error page when not in debug mode.
  4. Visit http://localhost:8888/Webucator/php/ExceptionHandling/Exercises/phppoetry.com/test-db-connect.php to test your code. You should get a "Success" message if the connection succeeds.

Solution:

ExceptionHandling/Solutions/phppoetry.com/includes/utilities.php
<?php 
  require_once 'config.php';

  function isProduction() {
    // Provide way of knowing if the code is on production server
    return false;
  }

  function isDebugMode() {
    // You may want to provide other ways for setting debug mode
    return !isProduction();
  }

  function dbConnect() {
    $dbConfig = getDbConfig();
    $dsn = $dbConfig['dsn'];
    $username =  $dbConfig['un'];
    $password =  $dbConfig['pw'];

    try {
      $db = new PDO($dsn, $username, $password);
      return $db;
    } catch (PDOException $e) {
      // log error
      logError($e, true);
      return false;
    }
  }

  function logError($e, $redirect=false) { 
    $errorType = gettype($e);
    switch ($errorType) {
      case 'string':
        $msg = $e;
        break;
      default:
        $msg = $e->getMessage() . ' in ' . $e->getFile() . 
          ' on line ' . $e->getLine();
    }
    error_log($msg); // php_error.log

    if (isDebugMode()) {
      echo "<h3 class='error'>For Developers' Eyes Only</h3>
        <div class='error'>$msg</div>";
    }

    if ($redirect && !isDebugMode()) {
      // Redirect to error page
      header("Location: error-page.php");
    }
  }
?>

When Queries Fail to Execute

The PDOStatement's execute() method returns true if the database query succeeds and false if the query fails. When a query fails, you can get information on the cause of the failure by calling the PDOStatement's errorInfo() method, which returns an array containing:

  1. A SQLSTATE error code.
  2. A driver-specific error code.
  3. A driver-specific error message.

The last of these generally gives you the information you need to debug your query. Take a look at the following code:

Code Sample:

ExceptionHandling/Demos/query-failure.php
<?php
  require_once '../Solutions/phppoetry.com/includes/utilities.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../static/styles/normalize.css">
<link rel="stylesheet" href="../../static/styles/styles.css">
<title>Query Failure</title>
</head>
<body>
<main>
<?php
  $db = dbConnect();
  try {
    // users table doesn't have user_name field
    $query = 'SELECT user_name FROM users';
    $stmt = $db->prepare($query);
    if (!$stmt->execute()) {
      // The query failed
      $errorMsg = $stmt->errorInfo()[2] . ": $query";
      logError($errorMsg, true);
    }
  } catch (PDOException $e) {
    // Some error occurred with our database communication
    logError($e->getMessage(), true);
  }

  // If we get here without an error,
  //   we can safely fetch data from $stmt
?>
</main>
</body>
</html>

Code Explanation

Visit http://localhost:8888/Webucator/php/ExceptionHandling/Demos/query-failure.php to test this page. It should look like this:Query Failure

Things to note:

  1. We are intentionally trying a query that will break: there is no user_name field in the users table.
  2. If the query fails (and it will), $stmt->execute() will return false, so !$stmt->execute() will return true. This is not an exception in our PHP code. It is a database error; it is MySQL telling PHP that the query is bad. We log $stmt->errorInfo()[2] to provide error information for the developer.
  3. It is also possible that something goes wrong with the communication between PHP and the database. For example, the database could go down between the time that we connected to it and the time we ran this query. In this case, a PDOException will occur. That's why we put the whole thing in a try/catch block.
  4. If no PDOException occurs and the query succeeds, the PDOStatement object will then have the data from the database and we can safely fetch it.

Catching Errors in the PHP Poetry Website

Duration: 40 to 60 minutes.

In this exercise, you will change the code to use the dbConnect() function in utilities.php. You will also wrap all the code that prepares and executes SQL statements in error-catching code.

  1. Add code to includes/header.php to connect to the database and store the connection in $db unless $db has already been set.
  2. Open index.php in your editor and review the code. Notice that we have removed the database connection code (shown below) and that we have wrapped the prepare() and execute() calls in a try/catch block. You must do the same in poems.php and poem.php:
    $dsn = 'mysql:host=localhost;dbname=poetree';
    $username = 'root';
    $password = 'pwdpwd';
    $db = new PDO($dsn, $username, $password);
  3. In poem.php, we need to connect to and query the database before including header.php, so that we can include the name of the poem in the title of the page. Set $db in poem.php. Note that this is why in header.php we only set $db if it hasn't already been set.
  4. Visit http://localhost:8888/Webucator/php/ExceptionHandling/Exercises/phppoetry.com/index.php and navigate around to test the site. If you get any errors, they should be reported to the browser in a way that helps you fix them.

Solution:

ExceptionHandling/Solutions/phppoetry.com/includes/header.php
<?php
  require_once 'config.php';
  require_once 'utilities.php';
  if (isDebugMode()) {
    ini_set('display_errors', '1');
  }

  // If $db isn't already set, set it.
  if (!isset($db)) {
    $db = dbConnect();
  }
  
  $pageTitleTag = empty($pageTitle)
              ? 'The Poet Tree Club'
              : $pageTitle . ' | The Poet Tree Club';
?>
---- C O D E   O M I T T E D ----

Solution:

ExceptionHandling/Solutions/phppoetry.com/index.php
<?php
  require 'includes/header.php';
  
  $query = "SELECT p.poem_id, p.title, p.date_approved, 
  c.category, u.username
          FROM poems p
          JOIN categories c ON c.category_id = p.category_id
          JOIN users u ON u.user_id = p.user_id
          WHERE p.date_approved IS NOT NULL
          ORDER BY p.date_approved DESC
          LIMIT 0, 3";
          
  try {
    $stmt = $db->prepare($query);
    if (!$stmt->execute()) {
      $errorMsg = $stmt->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }
?>
---- C O D E   O M I T T E D ----

Solution:

ExceptionHandling/Solutions/phppoetry.com/poems.php
---- C O D E   O M I T T E D ----
  try {
    $stmt = $db->prepare($query);
    if (!$stmt->execute($params)) {
      $errorMsg = $stmt->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }

  $qPoemCount = "SELECT COUNT(p.poem_id) AS num
  FROM poems p
    JOIN categories c ON c.category_id = p.category_id
    JOIN users u ON u.user_id = p.user_id
  WHERE p.date_approved IS NOT NULL";

  if ($whereConditions) {
    $where = implode($whereConditions, ' AND ');
    $qPoemCount .= ' AND ' . $where;
  }

  try {
    $stmtPoemCount = $db->prepare($qPoemCount);
    if (!$stmtPoemCount->execute($params)) {
      $errorMsg = $stmtPoemCount->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }
  $poemCount = $stmtPoemCount->fetch()['num'];

  $prevOffset = max($offset - $rowsToShow, 0);
  $nextOffset = $offset + $rowsToShow;

  $qCategories = "SELECT c.category_id, c.category,
    COUNT(p.poem_id) AS num_poems
  FROM categories c
    JOIN poems p ON c.category_id = p.category_id
  WHERE p.date_approved IS NOT NULL
  GROUP BY c.category_id
  ORDER BY c.category";

  try {
    $stmtCats = $db->prepare($qCategories);
    if (!$stmtCats->execute()) {
      $errorMsg = $stmtCats->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }

  $qUsers = "SELECT u.user_id, u.username, 
    COUNT(p.poem_id) AS num_poems
  FROM users u
    JOIN poems p ON u.user_id = p.user_id
  WHERE p.date_approved IS NOT NULL
  GROUP BY u.user_id
  ORDER BY u.username";

  try {
    $stmtUsers = $db->prepare($qUsers);
    if (!$stmtUsers->execute()) {
      $errorMsg = $stmtUsers->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }
---- C O D E   O M I T T E D ----

Solution:

ExceptionHandling/Solutions/phppoetry.com/poem.php
<?php
  require_once 'config.php';
  require_once 'includes/utilities.php';
  $db = dbConnect();
  $poemId = $_GET['poem-id'];
  $query = "SELECT u.username, u.user_id,
    p.title, p.poem, p.date_submitted, p.date_approved,
    c.category_id, c.category
    FROM users u
      JOIN poems p ON u.user_id = p.user_id
      JOIN categories c ON c.category_id = p.category_id
    WHERE p.poem_id = ?";

  try {
    $stmt = $db->prepare($query);
    if (!$stmt->execute([$poemId])) {
      $errorMsg = $stmt->errorInfo()[2] . ": $query";
      logError($errorMsg);
    }
  } catch (PDOException $e) {
    logError($e->getMessage(), true);
  }
  $row = $stmt->fetch();
---- C O D E   O M I T T E D ----