Skip to content
Advertisement

What are the best practices for catching and re-throwing exceptions?

Should caught exceptions be re-thrown directly, or should they be wrapped around a new exception?

That is, should I do this:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

or this:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

If your answer is to throw directly please suggest the use of exception chaining, I am not able to understand a real world scenario where we use exception chaining.

Advertisement

Answer

You should not be catching the exception unless you intend to do something meaningful.

“Something meaningful” might be one of these:

Handling the exception

The most obvious meaningful action is to handle the exception, e.g. by displaying an error message and aborting the operation:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Logging or partial cleanup

Sometimes you do not know how to properly handle an exception inside a specific context; perhaps you lack information about the “big picture”, but you do want to log the failure as close to the point where it happened as possible. In this case, you may want to catch, log, and re-throw:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

A related scenario is where you are in the right place to perform some cleanup for the failed operation, but not to decide how the failure should be handled at the top level. In earlier PHP versions this would be implemented as

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 has introduced the finally keyword, so for cleanup scenarios there is now another way to approach this. If the cleanup code needs to run no matter what happened (i.e. both on error and on success) it’s now possible to do this while transparently allowing any thrown exceptions to propagate:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Error abstraction (with exception chaining)

A third case is where you want to logically group many possible failures under a bigger umbrella. An example for logical grouping:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

In this case, you do not want the users of Component to know that it is implemented using a database connection (maybe you want to keep your options open and use file-based storage in the future). So your specification for Component would say that “in the case of an initialization failure, ComponentInitException will be thrown”. This allows consumers of Component to catch exceptions of the expected type while also allowing debugging code to access all the (implementation-dependent) details.

Providing richer context (with exception chaining)

Finally, there are cases where you may want to provide more context for the exception. In this case it makes sense to wrap the exception in another one which holds more information about what you were trying to do when the error occurred. For example:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

This case is similar to the above (and the example probably not the best one could come up with), but it illustrates the point of providing more context: if an exception is thrown, it tells us that the file copy failed. But why did it fail? This information is provided in the wrapped exceptions (of which there could be more than one level if the example were much more complicated).

The value of doing this is illustrated if you think about a scenario where e.g. creating a UserProfile object causes files to be copied because the user profile is stored in files and it supports transaction semantics: you can “undo” changes because they are only performed on a copy of the profile until you commit.

In this case, if you did

try {
    $profile = UserProfile::getInstance();
}

and as a result caught a “Target directory could not be created” exception error, you would have a right to be confused. Wrapping this “core” exception in layers of other exceptions that provide context will make the error much easier to deal with (“Creating profile copy failed” -> “File copy operation failed” -> “Target directory could not be created”).

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement