捕获和重新抛出异常的最佳做法是什么?

时间:2011-04-05 12:18:47

标签: php exception

是否应该直接重新抛出捕获的异常,或者它们是否应该包含新的异常?

那就是我应该这样做:

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

或者这个:

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

如果您的答案是直接抛出,请建议使用异常链接,我无法理解我们使用异常链接的真实场景。

6 个答案:

答案 0 :(得分:255)

除非您打算做一些有意义的事情,否则您不应该捕获异常

“有意义的东西”可能就是其中之一:

处理异常

最明显的有意义的行动是处理异常,例如:通过显示错误消息并中止操作:

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

记录或部分清理

有时您不知道如何在特定上下文中正确处理异常;也许你缺乏关于“大图”的信息,但你确实希望将故障记录在尽可能接近发生点的位置。在这种情况下,您可能想要捕获,记录和重新抛出:

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

相关场景是您在正确的位置为失败的操作执行一些清理,但不决定如何在顶层处理失败。在早期的PHP版本中,这将实现为

$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引入了finally关键字,因此对于清理方案,现在有另一种方法可以解决这个问题。如果清理代码无论发生什么都需要运行(即错误和成功),现在可以在透明地允许任何抛出的异常传播的情况下执行此操作:

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

错误抽象(带有异常链接)

第三种情况是,您希望在更大的保护伞下对许多可能的故障进行逻辑分组。逻辑分组的一个示例:

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);
        }
    }
}

在这种情况下,您不希望Component的用户知道它是使用数据库连接实现的(可能您希望保持选项打开并在将来使用基于文件的存储)。因此,您对Component的规范会说“在初始化失败的情况下,ComponentInitException将被抛出”。这允许Component的使用者捕获预期类型的异常,同时还允许调试代码访问所有(依赖于实现的)详细信息

提供更丰富的上下文(异常链接)

最后,在某些情况下,您可能希望为异常提供更多上下文。在这种情况下,将异常包装在另一个异常中是有意义的,该异常包含有关错误发生时您尝试执行的操作的更多信息。例如:

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);
        }
    }
}

这种情况与上面类似(并且这个例子可能不是最好的例子),但它说明了提供更多上下文的重点:如果抛出异常,它会告诉我们文件复制失败。但为什么失败了?此信息在包装的异常中提供(如果示例更复杂,则可能有多个级别。)

如果您考虑以下情况,可以说明这样做的价值。创建UserProfile对象会导致文件被复制,因为用户配置文件存储在文件中并且它支持事务语义:您可以“撤消”更改,因为它们仅在您提交之前在配置文件的副本上执行。

在这种情况下,如果你做了

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

因此捕获了“无法创建目标目录”异常错误,您将有权被混淆。在提供上下文的其他异常层中包含此“核心”异常将使错误更容易处理(“创建配置文件复制失败” - >“文件复制操作失败” - >“无法创建目标目录” )。

答案 1 :(得分:29)

嗯,这都是关于维护抽象的。所以我建议使用异常链接直接抛出。至于原因,让我解释一下leaky abstractions

的概念

假设您正在构建模型。该模型应该从应用程序的其余部分抽象出所有数据持久性和验证。那么现在当你遇到数据库错误时会发生什么?如果你重新抛出DatabaseQueryException,你就会泄露抽象。要理解为什么,请考虑一下抽象。你不关心模型存储数据的 ,就像它一样。同样,你并不关心模型底层系统出了什么问题,只是你知道出了什么问题,大概出了什么问题。

因此,通过重新抛出DatabaseQueryException,您正在泄漏抽象并要求调用代码理解模型下正在发生的事情的语义。相反,创建一个通用的ModelStorageException,并将捕获的DatabaseQueryException包装在其中。这样,您的调用代码仍然可以尝试在语义上处理错误,但是模型的基础技术并不重要,因为您只是暴露了该抽象层的错误。更好的是,因为你包装了异常,如果它一直冒出来并且需要记录,你可以追踪抛出的根异常(走链),这样你仍然可以获得所需的所有调试信息!

除非您需要进行一些后期处理,否则不要简单地捕获并重新抛出相同的异常。但像} catch (Exception $e) { throw $e; }这样的街区毫无意义。但是你可以重新包装异常以获得一些重要的抽象收益。

答案 2 :(得分:8)

恕我直言,抓住一个例外来重新抛出它是无用的。在这种情况下,只是不要捕获它,并让之前调用的方法处理它(也称为调用堆栈中'upper'的方法)

如果你重新抛出它,将捕获的异常链接到你将抛出的新异常绝对是一个好习惯,因为它将保留捕获的异常包含的信息。但是,重新抛出它只有在为捕获的异常添加一些信息或处理时才有用,可能是某些上下文,值,记录,释放资源等等。

添加一些信息的方法是扩展Exception类,以获得NullParameterExceptionDatabaseException等异常。此外,这允许开发者只捕获一些例外他可以处理。例如,只能捕获DatabaseException并尝试解决导致Exception的原因,例如重新连接到数据库。

答案 3 :(得分:1)

你通常会这样想。

一个类可能抛出许多不匹配的异常类型。因此,您为该类或类的类创建一个异常类并抛出它。

因此,使用该类的代码只能捕获一种类型的异常。

答案 4 :(得分:1)

您必须查看Exception Best Practices in PHP 5.3

PHP中的异常处理不是任何延伸的新功能。在以下链接中,您将看到PHP 5.3中基于异常的两个新功能。第一个是嵌套异常,第二个是SPL扩展提供的一组新的异常类型(现在是PHP运行时的核心扩展)。这两个新功能都已进入最佳最佳实践,并值得详细研究。

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3

答案 5 :(得分:1)

我们有可能产生异常的 try/catch - 因为

  • 在您的应用中做出合乎逻辑的决策 - 如果您根据异常类型做出合乎逻辑的决策,您可能需要更改异常的类型。但是抛出的异常类型大部分时间都足够具体。所以这更像是一个不对异常做任何事情的理由,只是重新抛出它。

  • 向用户展示不同的东西 - 在这种情况下,只需按原样重新抛出并决定在最高级别做什么 - 在控制器中。您需要更改消息 - 记录真实的技术消息并向用户显示友好的消息。

  • DB 事务 - 它可以属于上述 2 种类型中的任何一种 - 如果您做出一些合乎逻辑的决定或者您只需要告诉用户一些事情。

所以,除非你有很好的理由,否则应该只在一个地方处理异常(否则会变得混乱)——而且那个地方必须是最顶层——在控制器中。

所有其他地方都应该被视为中介,你应该只是冒泡例外 - 除非你有很好的理由不这样做。