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

Posted

技术标签:

【中文标题】捕获和重新抛出异常的最佳实践是啥?【英文标题】:What are the best practices for catching and re-throwing exceptions?捕获和重新抛出异常的最佳实践是什么? 【发布时间】:2011-07-29 22:18:35 【问题描述】:

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

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

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/catch - 因为

在您的应用中做出合乎逻辑的决定 - 如果您根据异常类型做出合乎逻辑的决定,您可能希望更改异常的类型。但是抛出的异常类型大多数时候都足够具体。所以这更多是一个不对异常做任何事情的理由,只是重新抛出它。

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

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

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

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

【讨论】:

【参考方案2】:

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

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

处理异常

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

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/ 记录或清理:使用 finally 并在数据层上方记录异常跨度> @remi:除了 PHP 不支持 finally 构造(至少现在还不支持)......所以这已经结束了,这意味着我们必须求助于像这样的肮脏的东西...... @remibourgarel: 1: 这只是一个例子。当然你不应该在这个级别上这样做,但答案已经足够长了。 2:正如@ircmaxell 所说,PHP 中没有finally 终于,PHP 5.5 现在终于实现了。 我认为您在这里的列表中错过了一个原因 - 您可能无法判断您是否可以处理异常,直到您发现它并有机会检查它。例如,使用错误代码(并且有无数错误代码)的较低级别 API 的包装器可能有一个异常类,它会针对任何错误抛出一个实例,并带有一个可以检查以获取的 error_code 属性底层错误代码。如果您只能有意义地处理其中一些错误,那么您可能想要捕获、检查,如果您无法处理错误 - 重新抛出。【参考方案3】:

恕我直言,捕获异常以重新抛出它是无用。在这种情况下,不要抓住它,让之前调用的方法处理它(也就是调用堆栈中“上层”的方法)

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

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

【讨论】:

这不是没用的,有时候你需要在抛出异常的函数中做一些事情,然后重新抛出它,让更高层的 catch 做其他事情。在我正在处理的一个项目中,我们有时会在操作方法中捕获异常,向用户显示友好的通知,然后重新抛出它,以便代码中更远的 try catch 块可以再次捕获它以将错误记录到日志。 正如我所说,您向异常添加一些信息(显示通知,记录它)。您不会像在 OP 的示例中那样只是重新抛出它 好吧,如果你需要关闭资源,你可以重新抛出它,但没有额外的信息要添加。我同意这不是世界上最干净的东西,但它并不可怕 @ircmaxell 同意,已编辑以反映只有当你除了重新扔它之外什么都不做时它是无用的 重要的是,通过重新抛出异常,您丢失了最初抛出异常的文件和/或行信息。所以通常最好扔一个新的并传递旧的,就像问题的第二个例子一样。否则它只会指向 catch 块,让您猜测实际问题是什么。【参考方案4】:

嗯,这一切都是为了保持抽象。所以我建议使用异常链直接抛出。至于为什么,我解释一下leaky abstractions的概念

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

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

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

【讨论】:

很好的答案。似乎 Stack Overflow 周围的很多人(基于答案等)都在使用它们错误。【参考方案5】:

你通常是这样想的。

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

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

【讨论】:

嘿,您能否提供更多详细信息或链接,我可以在其中阅读有关此方法的更多信息。

以上是关于捕获和重新抛出异常的最佳实践是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Java 处理异常 9 个最佳实践

定义自己的异常类的最佳实践?

异步编程最佳实践

最佳实践:从属性中抛出异常

Java异常处理和最佳实践(含案例分析)

Java Exception最佳实践(转)