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

2022-08-30 06:32:45

捕获的异常应该直接重新引发,还是应该围绕新的异常进行包装?

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

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

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


答案 1

除非你打算做一些有意义的事情,否则你不应该抓住异常。

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

处理异常

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

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

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

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

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

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

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

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

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

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


推荐