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


156

应该将捕获的异常直接重新抛出,还是将它们包装在新的异常周围?

也就是说,我应该这样做:

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

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

Answers:


287

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

“有意义的事情”可能是其中之一:

处理异常

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

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

并且导致捕获到“无法创建目标目录”异常错误,您将有权利感到困惑。在提供上下文的其他异常层中包装此“核心”异常将使错误更易于处理(“创建配置文件复制失败”->“文件复制操作失败”->“无法创建目标目录”)。


我同意只有最后2个原因:1 /处理异常:你不应该在这个层面上,2 /日志或清理做到这一点:使用最后并记录您的数据层上面的例外
雷米bourgarel

1
@remi:只是PHP不支持该finally构造(至少现在还不支持)...所以,这已经出来了,这意味着我们必须诉诸于这种肮脏的事情……
ircmaxell 2011年

@remibourgarel:1:这只是一个例子。当然,您不应该在此级别上这样做,但是答案足够长了。2:正如@ircmaxell所说,finallyPHP中没有。
乔恩

3
终于,PHP 5.5现在终于实现了。
OCDev

12
我认为您在这里的列表中错过了一个原因-您可能无法知道是否可以处理异常,除非您发现了异常并有机会对其进行检查。例如,使用错误代码(并且有不计其数的错误代码)的较低级API的包装器可能只有一个异常类,该异常类会抛出任何错误的实例error_code,并且可以检查该属性以获取基础错误码。如果您只能有意义地处理其中一些错误,那么您可能想捕获,检查,如果不能处理错误,请重新抛出。
Mark Amery 2014年

37

好吧,这都是关于维护抽象的。因此,我建议使用异常链接直接抛出异常。至于为什么,让我解释一下泄漏抽象的概念

假设您正在建立模型。该模型应该从应用程序的其余部分中抽象出所有数据持久性和验证。那么,当您遇到数据库错误时会发生什么呢?如果您将扔掉了DatabaseQueryException,那么您将泄漏抽象。要了解原因,请考虑一下抽象。您不在乎模型如何存储数据,仅是数据存储。同样,您不必在乎模型的基础系统中到底出了什么问题,只是知道有什么问题,以及大概有什么问题。

因此,通过抛出DatabaseQueryException,您将泄漏抽象并要求调用代码了解该模型下正在发生的事情的语义。相反,创建一个泛型ModelStorageException,然后将捕获的内容包装在DatabaseQueryException其中。这样,您的调用代码仍然可以尝试从语义上处理错误,但是,该模型的基础技术无关紧要,因为您仅从该抽象层公开错误。更好的是,由于您包装了异常,如果异常一直冒出来并需要记录,则可以跟踪引发的根异常(遍历整个链),因此您仍然拥有所需的所有调试信息!

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


2
好答案。似乎围绕Stack Overflow的很多人(基于答案等)都在错误地使用它们。
詹姆斯

8

恕我直言,捕获异常以将其重新抛出是没有用的。在这种情况下,只是不要捕获它,而是让先前调用的方法来处理它(也就是在调用堆栈中位于“上”的方法)

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

添加一些信息的方式是扩展Exception类,有像例外NullParameterExceptionDatabaseException等更多了,这让developper只赶上一些例外,他可以处理。例如,只能捕获DatabaseException并尝试解决引起的原因Exception,例如重新连接到数据库。


2
它不是没有用的,有时候您需要在抛出异常的函数中执行某些操作,然后将其重新抛出以使更高级别的捕获执行其他操作。在我正在从事的一个项目中,有时我们会在操作方法中捕获异常,向用户显示友好通知,然后将其重新抛出,因此代码中更远的try catch块可以再次捕获该错误以将错误记录到一个日志。
MitMaro 2011年

1
因此,正如我所说,您向异常添加了一些信息(显示通知,记录日志)。您不只是像OP的示例中那样将其重新抛出
克莱门特·赫雷曼

2
好吧,如果您需要关闭资源,但是没有其他信息可以添加,则可以将其重新抛出。我同意这不是世界上最干净的东西,但这并不可怕
ircmaxell

2
@ircmaxell同意进行编辑,以反映出它只有在您除了将其重新
Clement Herreman

1
重要的一点是,通过重新抛出该异常,您可以松开该文件和/或行的信息,以了解最初引发异常的位置。因此,通常最好是抛出一个新的,然后传递旧的,就像在问题的第二个示例中一样。否则,它只会指向catch块,让您猜测实际的问题是什么。
DanMan 2014年


1

您通常是这样想的。

一个类可能会抛出许多不匹配的异常。因此,您可以为该类或类的类型创建一个异常类,然后将其抛出。

因此,使用该类的代码仅必须捕获一种类型的异常。


1
嘿,您能否提供更多详细信息或链接,以便在其中阅读有关此方法的更多信息。
拉胡尔·普拉萨德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.