【问题标题】:Theory on error handling?错误处理理论?
【发布时间】:2010-12-31 16:21:25
【问题描述】:

关于错误处理的大多数建议都归结为一些技巧和窍门(例如,参见this post)。这些提示很有帮助,但我认为它们并不能回答所有问题。我觉得我应该根据某种哲学来设计我的应用程序,这是一种为构建提供坚实基础的思想流派。关于错误处理的话题有这样的理论吗?

这里有几个实际问题:

  • 如何决定错误是应该在本地处理还是传播到更高级别的代码?
  • 如何决定是记录错误还是向用户显示错误消息?
  • 日志记录是否应该只在应用程序代码中完成?或者可以从库代码中进行一些日志记录。
  • 如果出现异常,您应该一般在哪里捕获它们?在低级或高级代码中?
  • 你是应该争取通过所有代码层的统一错误处理策略,还是尝试开发一个可以适应各种错误处理策略的系统(以便能够处理来自 3rd 方库的错误) .
  • 创建错误代码列表是否有意义?还是现在已经过时了?

在许多情况下,常识足以制定一个足够好的策略来处理错误情况。但是,我想知道是否有更正式/“学术”的方法?

PS:这是一个普遍的问题,但也欢迎 C++ 特定的答案(C++ 是我工作的主要编程语言)。

【问题讨论】:

  • 社区维基?这里没有正确的答案。
  • 虽然这里一个首要问题(“是否有指导错误/异常处理设施设计的哲学?”),但它非常模糊且难以回答,并且所有实际答案都在您列表中的各个子问题上。因此,我认为这不是一个好的 SO 问题,也不知道如何挽救它。所以我投票给 Not a Real Question。
  • @Dervin:75% 并不低,并不是每个问题都有明确的答案。
  • 里面有两个问题:一个是关于错误处理的,一个是关于日志的。是的,这是专门记录错误情况,但它仍然不同:具体而言,错误处理通常意味着自动和立即处理,而日志要么需要手动审查和/或异步处理(提取工具)。我建议您将这个问题缩小到任一方面,并​​提出与另一方面相关的专门单独问题。
  • 定义你理解的“错误”是什么意思:由于验证/测试不佳?由于硬件/连接故障?由于资源管理不善? ... - 每个“类别”的错误都将决定它自己的“处理”需求,这些需求可能会重叠,也可能不会重叠。我不认为通用处理程序是可定义的。

标签: c++ error-handling


【解决方案1】:

第一个问题可能是你能对错误做些什么?

你能修复它(在这种情况下你需要告诉用户)还是用户可以修复它?

如果没有人可以修复它并且您要退出,那么将其报告给您是否有任何价值(通过故障转储或错误代码)?

【讨论】:

    【解决方案2】:

    正在记录应该只记录的内容 在应用程序代码中完成?或者是 可以从图书馆做一些日志记录 代码。

    只是想对此发表评论。我的观点是永远不要直接在库代码中记录日志,而是在应用程序代码中提供挂钩或回调来实现这一点,以便应用程序可以决定如何处理来自日志的输出(如果有的话)。

    【讨论】:

      【解决方案3】:

      如何决定错误应该在本地处理还是传播到更高级别的代码?

      如果异常中断了方法的操作,那么将其抛出到更高级别是一个好方法。如果您熟悉 MVC,则必须在 Controller 中评估异常。

      如何决定是记录错误还是向用户显示错误消息? 记录错误和有关错误的所有可用信息是一个好方法。如果错误中断了操作或用户需要知道发生了错误,则应将其显示给用户。请注意,在 windows 服务中日志是非常非常重要的。

      记录应该只在应用程序代码中完成吗?或者可以从库代码中进行一些日志记录。

      我看不出有任何理由在 dll 中记录错误。它应该只抛出错误。当然,这样做可能有特定的原因。在我们公司,一个 dll 记录有关进程的信息(不仅是错误)

      如果出现异常,一般应该在哪里捕获它们?在低级或高级代码中? 类似问题:您应该在什么时候停止传播错误并进行处理?

      在控制器中。

      编辑:如果您不熟悉 MVC,我需要稍微解释一下。模型视图控制器是一种设计模式。在模型中,您开发应用程序逻辑。在视图中,您向用户显示内容。在 Controller 中,您获取用户事件并调用 Model 以获得相关功能,然后调用 View 以向用户显示结果。

      假设您有一个包含两个文本框、一个标签和一个名为“添加”的按钮的表单。正如你可能猜到的,这是你的观点。 Button_Click 事件在 Controller 中定义。并且在 Model 中定义了一个 add 方法。当用户点击时,会触发 Button_Click 事件,Controller 会调用 add 方法。这里的文本框值可以是空的,也可以是字母而不是数字。 add 函数发生异常并抛出此异常。控制器处理它。并在标签中显示错误信息。

      你是应该通过各个层的代码争取统一的错误处理策略,还是尝试开发一个能够适应多种错误处理策略的系统(为了能够从3rd开始处理错误)党图书馆)。

      我更喜欢第二个。这会更容易。而且我认为您不能为错误处理做一般的事情。特别是对于不同的库。

      创建错误代码列表有意义吗?还是现在已经过时了?

      这取决于您将如何使用它。在单个应用程序(网站、桌面应用程序)中,我认为不需要它。但是如果你开发一个 Web 服务,你将如何通知用户错误呢?在这里提供错误代码始终很重要。

      If (error.Message == "User Login Failed")
      {
         //do something.
      }
      
      If (error.Code == "102")
      {
         //do something.
      }
      

      你更喜欢哪一个?

      现在还有另一种错误代码方法:

      If (error.Code == "LOGIN_ERROR_102") // wrong password
      {
         //do something.
      }
      

      其他可能是:LOGIN_ERROR_103(例如:这是用户已过期)等...

      这也是人类可读的。

      【讨论】:

        【解决方案4】:

        这是一篇很棒的博客文章,它解释了应该如何进行错误处理。 http://damienkatz.net/2006/04/error_code_vs_e.html

        如何决定错误应该在本地处理还是传播到更高级别的代码? 就像 Martin Becket 在另一个答案中所说的那样,这是一个错误是否可以在这里修复的问题。

        如何决定是记录错误还是将其作为错误消息显示给用户? 如果您这样认为,您可能永远不应该向用户显示错误。相反,向他们展示一个格式良好的信息来解释情况,而不提供太多技术信息。然后记录技术信息,尤其是在处理输入时出现错误时。如果您的代码不知道如何处理错误输入,那么必须修复。

        日志记录是否应该只在应用程序代码中完成?还是可以从库代码中进行一些日志记录。 登录库代码没有用,因为您甚至可能没有编写它。但是,应用程序可以记录与库代码的交互,甚至可以通过统计信息检测错误。

        如果出现异常,一般应该在哪里捕获它们?在低级或高级代码中? 见问题一。

        类似的问题:你应该在什么时候停止传播错误并处理它? 见问题一。

        你是应该争取通过所有层级的代码来统一错误处理策略,还是尝试开发一个可以自己适应各种错误处理策略的系统(为了能够从3rd开始处理错误)党图书馆)。 在大多数繁重的语言中,抛出异常是一项昂贵的操作,因此在整个程序流程因该操作而中断的情况下使用它们。另一方面,如果您可以预测函数的所有结果,则将任何数据放入作为参数传递给它的引用变量,并返回错误代码(成功时为 0,错误时为 1+)。

        创建错误代码列表有意义吗?还是现在已经过时了? 为特定函数制作一个错误代码列表,并将其记录为可能的返回值列表。请参阅上一个问题以及链接。

        【讨论】:

        • 链接很有趣。
        【解决方案5】:

        我对从库代码中记录(或其他操作)的看法是从不。

        图书馆不应将政策强加于其用户,而用户可能有意发生错误。也许程序故意请求一个特定的错误,期望它到达,以测试一些条件。记录此错误会产生误导。

        日志记录(或其他任何东西)将策略强加给调用者,这是不好的。此外,如果一个无害的错误条件(例如,调用者会忽略或无害地重试)以高频率发生,那么大量日志可能会掩盖任何合法错误或导致健壮性问题(填充磁盘、使用过多的 IO等)

        【讨论】:

        • 正如@RA 提到的,我相信某些库提供一种从更高级别代码附加记录器的机制会很有用。例如,如果您正在编写支持不同详细级别的控制台应用程序,这可能会很方便。
        【解决方案6】:

        我的两分钱。

        如何决定错误是应该在本地处理还是传播到更高级别的代码? 处理您可以处理的错误。让错误传播你不能传播。

        如何决定是记录错误还是向用户显示错误消息? 两个正交的问题,它们并不相互排斥。记录错误最终是为您开发的。如果您对此感兴趣,请登录。如果用户可以操作,则将其显示给用户(“错误:无网络连接!”)。

        记录只能在应用程序代码中完成吗?或者可以从库代码中进行一些日志记录。 我看不出图书馆无法登录的原因。

        如果出现异常,一般应该在哪里捕获它们?在低级或高级代码中? 您应该在可以处理它们的地方捕获它们(插入您对句柄的定义)。如果您无法处理它们,请忽略它们(也许堆栈中的某个人可以处理它们..)。

        您当然不应该在您调用的每个抛出函数周围放置一个 try/catch 块。

        类似的问题:你应该在什么时候停止传播错误并处理它? 您是应该通过所有代码层来争取统一的错误处理策略,还是尝试开发一个可以适应各种错误处理策略的系统(以便能够处理来自 3rd 方库的错误)。强> 首先,您可以实际处理它。该点可能不存在,您的应用程序可能会崩溃。然后你会得到一个很好的故障转储,并且可以更新你的错误处理。

        创建错误代码列表有意义吗?还是现在已经过时了? 另一个争论点。我实际上会说不:所有错误代码的超级列表意味着该列表始终是最新的,因此当它不是最新的时,您实际上会造成伤害。最好让每个函数记录它可以返回的所有错误代码,而不是只有一个超级列表。

        【讨论】:

          【解决方案7】:

          我正在改变我的设计和编码理念,以便:

          1. 如果一切顺利,如预期的那样, 没有产生错误。
          2. 如果出现异常则抛出异常 不同的或意想不到的事情发生; 让调用者处理它。
          3. 如果无法解决,请传播 更上一层楼。

          希望通过这种技术,传播给用户的问题将非常重要;否则程序会尝试解决它们。

          我目前遇到了在返回码中丢失的问题;或创建新的返回码。

          【讨论】:

            【解决方案8】:

            Krzysztof Cwalina 和 Brad Abrams 所著的“框架设计指南:可重用 .NET 库的约定、惯用语和模式”一书对此提出了一些很好的建议。见第 7 章例外。例如,它倾向于抛出异常以返回错误代码。

            -克里普

            【讨论】:

            • .NET 中的一般做法不适用于 C++。在 .NET 中,也可能在作为源代码分发的 C++ 代码中,库要做的正确事情是提供两种变体。对于在 Windows 上作为二进制文件分发的 C++ 库,异常永远不是正确的方法,因为没有针对 C++ 异常的标准化 ABI。 C++ 模板提供了许多 .NET 不具备的可能性,例如内联应用程序提供的转换函数,该函数可以直接返回错误代码,将其包装在应用程序选择的类型中,或者抛出。
            【解决方案9】:
            1. 始终尽快处理。你越接近它的发生,你就越有可能做一些有意义的事情,或者至少弄清楚它发生在哪里以及为什么发生。在 C++ 中,这不仅仅是上下文问题,而且在许多情况下无法确定。

            2. 一般情况下,如果出现错误且是真正的错误(不是像找不到文件那样的错误,这并不是真正应该算作错误但被标记为错误的东西),您应该始终停止应用程序。它不会自行解决,一旦应用坏了就会导致无法调试的错误,因为它们与发生的区域无关。

            3. 为什么不呢?

            4. 见 1.

            5. 见 1.

            6. 你需要保持简单,否则你会后悔的。在运行时处理错误更重要的是进行测试以避免它们。

            7. 这就像说集中还是不集中更好。在某些情况下这可能很有意义,但在其他情况下会浪费时间。对于某种可加载的库/模块,可能会出现与数据相关的错误(垃圾输入,垃圾输出),这很有意义。对于更一般的错误处理或灾难性错误,请少用。

            【讨论】:

              【解决方案10】:

              错误处理没有形式理论。一个主题过于“特定于实现”而不能被视为科学领域(公平地说,编程本身是否是一门科学存在很大的争议)。

              尽管它是开发人员工作(以及他/她的生活)的重要组成部分,因此已针对该主题开发了实用方法和技术指导。

              A. Alexandrescu 在他的演讲 systematic error handling in C++

              中对这个主题提出了很好的看法

              我在GitHub 有一个存储库,其中实现了所介绍的技术。

              基本上,A.A 所做的就是实现一个类

              template<class T>
              class Expected { /* Implementation in the GitHub link */ };
              

              这意味着用作返回值。此类可以保存T 类型的返回值或异常(指针)。异常可以显式抛出,也可以根据请求抛出,但丰富的错误信息始终可用。一个示例用法是这样的

              int foo(); 
              // .... 
              Expected<int> ret = foo();
              if (ret.valid()) {
                  // do the work
              }
              else {
                  // either use the info of the exception 
                  // or throw the exception (eg in an exception "friendly" codebase)
              }
              

              在构建此错误处理框架时,A.A 将引导我们了解产生成功或不良错误处理的技术和设计,以及哪些有效或无效。他还给出了他对“错误”和“错误处理”的定义

              【讨论】:

                【解决方案11】:

                如何决定错误应该在本地处理还是传播到更高级别的代码?

                错误处理应在受影响的最高级别进行。如果它只影响较低级别的代码,那么它应该在那里处理。如果错误影响更高级别的代码,则需要在更高级别处理错误。这是为了防止一些更高级别的代码在错误导致其操作不正确后继续进行。如果受到影响,它应该知道发生了什么。

                如何决定是记录错误还是将其作为错误消息显示给用户?

                您应该始终记录错误。当用户受到错误影响时,您应该向用户显示错误。如果这是他们永远不会注意到并且没有直接影响的事情(例如,在第三个最终打开之前两个套接字未能打开,导致用户不应该报告很短的延迟),那么他们不应该被通知。

                记录应该只在应用程序代码中完成吗?或者可以从库代码中进行一些日志记录。

                过多的日志记录很少是坏事。当你不得不寻找一个库错误时,你会后悔没有记录事情,而不是在寻找其他错误时对额外的日志感到沮丧。

                如果出现异常,一般应该在哪里捕获它们?在低级或高级代码中?

                与上面的错误处理类似,它应该在影响的地方被捕获,并且可以有效地纠正/处理错误。这会因情况而异。

                你是应该通过各个层的代码争取统一的错误处理策略,还是尝试开发一个能够适应多种错误处理策略的系统(为了能够从3rd开始处理错误)党图书馆)。

                这在很大程度上是个人决定。我的内部错误处理与我用于任何涉及第三方库的错误处理有很大不同。我对我的代码的期望有一个大致的了解,但是第三方的东西可能会发生任何事情。

                创建错误代码列表有意义吗?还是现在已经过时了? 取决于您期望抛出多少错误。如果您花费大量时间寻找错误代码,您可能会喜欢您的错误代码列表,因为它们可以帮助您指出正确的方向。然而,任何花费在构建这些代码/错误修复上的时间都会减少,所以它是一个好坏参半的包。这在很大程度上取决于个人喜好。

                【讨论】:

                  【解决方案12】:

                  几年前我也想过同样的问题:)

                  在搜索和阅读了几篇文章后,我认为我找到的最有趣的参考资料是来自 Andy Longshaw 和 Eoin Woods 的 Patterns for Generation, Handling and Management of Errors。这是一次简短而系统的尝试,涵盖了您提到的基本习语和其他一些习语。

                  这些问题的答案颇具争议,但以上作者都勇敢地在会议上暴露自己,然后将他们的想法写在纸上。

                  【讨论】:

                  • 谢谢,贴出来的文章好像直接回答了我的问题。
                  • 如果链接失效,这里是backup
                  【解决方案13】:

                  免责声明:我不知道任何关于错误处理的理论,但是,当我探索各种语言和编程范式以及玩弄编程语言设计(并讨论他们)。因此,以下是我迄今为止的经验总结;有客观的论据。

                  注意:这应该涵盖所有问题,但我什至没有尝试按顺序解决它们,而是更喜欢结构化的演示文稿。为了清楚起见,在每个部分的末尾,我对它回答的问题给出了一个简洁的答案。


                  简介

                  作为前提,我想指出,无论讨论什么,在设计库(或可重用代码)时都必须牢记一些参数。

                  作者无法理解这个库将如何被使用,因此应该避免使集成变得更加困难的策略。最明显的缺陷是依赖全局共享状态;线程本地共享状态也可能是与协程/绿色线程交互的噩梦。使用这样的协程和线程也强调了同步最好留给用户,在单线程代码中这意味着没有(最佳性能),而在协程和绿色线程中,用户最适合实现(或使用现有的专用同步机制的实现。

                  话虽如此,当库仅供内部使用时,全局或线程局部变量可能很方便;如果使用,则应明确记录为技术限制。


                  记录

                  有很多方法可以记录消息:

                  • 带有额外信息,例如时间戳、进程 ID、线程 ID、服务器名称/IP...
                  • 通过同步调用或使用异步机制(和溢出处理机制)
                  • 在文件、数据库、分布式数据库、专用日志服务器...

                  作为库的作者,日志应该集成到客户端基础架构中(或关闭)。最好允许客户端提供钩子以便自己处理日志,我的建议是:

                  • 提供 2 个钩子:一个决定是否记录,一个用于实际记录(消息被格式化,后一个钩子仅在客户端决定记录时调用)
                  • 在消息的顶部提供:严重性(又名级别)、文件名、行和函数名(如果是开源的)或 逻辑模块(如果有多个)
                  • 默认情况下,写入stdoutstderr(取决于严重性),直到客户端明确表示不记录

                  我会注意到,按照介绍中描述的指南,同步留给客户端。

                  关于是否记录错误:不要记录(作为错误)您已经通过 API 报告的内容;但是,您仍然可以以较低的严重性记录详细信息。客户端可以在处理错误时决定是否报告,例如如果这只是一个推测调用,则选择不报告。

                  注意:某些信息不应进入日志,而其他一些信息最好进行混淆处理。例如,不应记录密码,最好对信用卡或护照/社会安全号码进行混淆(至少部分混淆)。在为此类敏感信息设计的库中,这可以在记录期间完成;否则应用程序应该处理这个问题。

                  日志记录是否应该只在应用程序代码中完成?还是可以从库代码中进行一些日志记录。

                  应用程序代码应该决定策略。库是否记录取决于它是否需要。


                  出错后继续?

                  在我们真正谈论报告错误之前,我们应该问的第一个问题是是否应该报告错误(以进行处理),或者事情是否严重到中止当前流程显然是最佳策略。

                  这当然是一个棘手的话题。一般来说,我会建议设计这样一个选项,如果需要,可以进行清除/重置。如果在某些情况下无法做到这一点,那么这些情况应该会导致流程中止。

                  注意:在某些系统上,可以获得进程的内存转储。如果应用程序处理敏感数据(密码、信用卡、护照等),最好在生产中停用它(但可以在开发过程中使用)。

                  注意:如果有一个调试开关,可以将部分错误报告调用转换为带有内存转储的中止,以帮助开发期间的调试,这可能会很有趣。


                  报告错误

                  错误的发生意味着函数/接口的契约不能被履行。这有几个后果:

                  • 应该警告客户端,这就是应该报告错误的原因
                  • 任何部分正确的数据都不应在野外逃逸

                  后一点将在稍后处理;现在让我们专注于报告错误。客户永远不能意外忽略此报告。这就是为什么使用错误代码是如此可恶的原因(在可以忽略返回值的语言中):

                  ErrorStatus_t doit(Input const* input, Output* output);
                  

                  我知道有两种方案需要对客户端部分进行明确的操作:

                  • 例外情况
                  • 结果类型(optional&lt;T&gt;either&lt;T, U&gt;、...)

                  前者是众所周知的,后者在函数式语言中被大量使用,并在 C++11 中以std::future&lt;T&gt; 为幌子引入,尽管存在其他实现。

                  我建议在可能的情况下更喜欢后者,因为它更容易理解,但在没有预期结果时恢复到异常。对比:

                  Option<Value&> find(Key const&);
                  
                  void updateName(Client::Id id, Client::Name name);
                  

                  updateName等“只写”操作的情况下,客户端对结果没有用处。它可以被引入,但很容易忘记检查。

                  当结果类型不切实际或不足以传达详细信息时,也会发生恢复异常:

                  Option<Value&> compute(RepositoryInterface&, Details...);
                  

                  在这种外部定义的回调的情况下,有几乎无限的潜在故障列表。实现可以使用网络、数据库、文件系统……在这种情况下,为了准确报告错误:

                  • 当接口不足以(或不切实际)传达错误的完整细节时,外部定义的回调应该通过异常报告错误。
                  • 基于此abstract 回调的函数应该对这些异常透明(让它们通过,未经修改)

                  目标是让这个异常冒泡到决定接口实现的层(至少),因为只有在这个层,才有机会正确解释抛出的异常。

                  注意:外部定义的回调并不强制使用异常,我们应该期待它可能会使用一些。


                  使用错误

                  为了使用错误报告,客户端需要足够的信息来做出决定。应该首选结构化信息,例如错误代码或异常类型(用于自动操作),并且可以以非结构化方式(供人类调查)提供附加信息(消息、堆栈等)。

                  最好有一个函数清楚地记录所有可能的故障模式:它们何时发生以及如何报告。但是,特别是在执行任意代码的情况下,客户端应该准备好处理未知代码/异常。

                  当然,结果类型是一个值得注意的例外:boost::variant&lt;Output, Error0, Error1, ...&gt; 提供了经过编译器检查的已知故障模式的详尽列表……当然,返回此类型的函数仍可能抛出异常。

                  如何决定是记录错误还是将其作为错误消息显示给用户?

                  当订单无法完成时,应始终警告用户,但应显示用户友好(可理解)的消息。如果可能,还应提供建议或解决方法。详情供调查小组使用。


                  从错误中恢复?

                  最后但同样重要的是,关于错误的真正可怕的部分是:恢复。

                  这是数据库(真实的)非常擅长的:类似事务的语义。如果发生任何意外情况,事务就会中止,就像什么都没发生一样。

                  在现实世界中,事情并不简单。脑海中浮现出取消发送电子邮件的简单示例:为时已晚。可能存在协议,具体取决于您的应用程序域,但这不在讨论范围内。不过,第一步是恢复正常的in-memory状态的能力;这在大多数语言中远非简单(而 STM 目前只能做这么多)。

                  首先是挑战的说明:

                  void update(Client& client, Client::Name name, Client::Address address) {
                      client.update(std::move(name));
                      client.update(std::move(address)); // Throws
                  }
                  

                  现在,地址更新失败后,我只剩下更新了一半的client。我能做什么?

                  • 尝试撤消发生的所有更新几乎是不可能的(撤消可能会失败)
                  • 在执行任何单个更新之前复制状态是一种性能消耗(假设我们甚至可以以可靠的方式将其交换回来)

                  在任何情况下,所需的簿记都会导致错误蔓延。

                  最糟糕的是:对于损坏的程度无法做出安全的假设(除了client 现在被搞砸了)。或者至少,没有假设会持续时间(和代码更改)。

                  通常,获胜的唯一方法就是不玩。


                  一种可能的解决方案:交易

                  在可能的情况下,关键思想是定义函数,这些函数要么失败,要么产生预期的结果。这些是我们的交易。而且它们的形式是不变的:

                  Either<Output, Error> doit(Input const&);
                  
                  // or
                  
                  Output doit(Input const&); // throw in case of error
                  

                  事务不会修改任何外部状态,因此如果它无法产生结果:

                  • 外部世界没有改变(没有什么可以回滚)
                  • 没有观察到的部分结果

                  任何不是事务的函数都应该被认为已经损坏了它所触及的任何东西,因此处理非事务函数错误的唯一理智方法是让它冒泡直到到达事务层。任何事先处理错误的尝试最终都注定要失败。

                  如何决定错误是应该在本地处理还是传播到更高级别的代码?

                  如果出现异常,您通常应该在哪里捕获它们?在低级或高级代码中?

                  只要安全这样做并且这样做有价值,就与他们打交道。最值得注意的是,捕获一个错误是可以的,检查它是否可以在本地处理,然后要么处理它,要么传递它。


                  你是应该通过所有代码层争取统一的错误处理策略,还是尝试开发一个可以适应各种错误处理策略的系统(以便能够处理来自 3rd 方库的错误) .

                  我之前没有解决这个问题,但是我相信很明显,我强调的方法已经是双重的,因为它包含结果类型和异常。因此,处理 3rd 方库应该是小菜一碟,尽管出于其他原因我建议无论如何都要包装它们(第 3 方代码更好地隔离在一个负责阻抗适应的面向业务的接口之外)。

                  【讨论】:

                  • 感谢您提供全面的概述。我还没有考虑过错误恢复(和事务)。
                  【解决方案14】:

                  简介

                  要了解错误处理需要做什么,我认为需要清楚地了解遇到的错误类型以及遇到错误的上下文。

                  对我来说,将两种主要类型的错误视为:

                  1. 不应该发生的错误,通常是由于代码中的错误。

                  2. 在正常操作中无法避免的预期错误,例如由于应用程序无法控制的数据库问题而导致数据库连接中断。

                  处理错误的方式很大程度上取决于错误的类型。

                  影响错误处理方式的不同上下文是:

                  • 应用代码

                  • 库代码

                  库代码中的错误处理与应用程序代码中的处理有些不同。

                  下面将讨论处理两种主要类型错误的原理。还解决了库代码的特殊注意事项。最后,在提出的哲学背景下解决了原帖中的具体实际问题。

                  错误类型

                  编程错误 - 错误 - 和其他不应该发生的错误

                  许多错误是编程错误的结果。这些错误通常无法纠正,因为无法预期特定的编程错误。这意味着我们无法提前知道错误使应用程序处于什么条件,因此我们无法从该条件中恢复并且不应该尝试。

                  最终,解决这种错误的方法是修复编程错误。为了促进这一点,错误应该尽快浮出水面。理想情况下,程序应在识别出此类错误并提供相关信息后立即退出。快速而明显的退出减少了完成调试和重新测试周期所需的时间,允许在相同的测试时间内修复更多的错误;这反过来会导致在部署时拥有更强大的应用程序,并且错误更少。

                  处理此类错误的另一个主要目标应该是提供足够的信息以便于识别错误。例如,在 Java 中,抛出 RuntimeException 通常会在堆栈跟踪中提供足够的信息来立即识别错误;在干净的代码中,通常可以仅通过检查堆栈跟踪来确定立即修复。在其他语言中,可能会记录调用堆栈或以其他方式保留必要的信息。至关重要的是不要为了简洁而隐瞒信息;发生此类错误时,不要担心您占用了多少日志空间。提供的信息越多,错误修复的速度就越快,当应用程序投入生产时,留下来污染日志的错误就越少。

                  服务器应用程序

                  现在,在某些服务器应用程序中,服务器具有足够的容错能力以在偶尔出现编程错误的情况下继续运行非常重要。在这种情况下,最好的方法是在必须继续运行的服务器代码和可以允许失败的任务处理代码之间进行非常明确的分离。例如,可以将任务归为线程或子进程,就像在许多 Web 服务器中所做的那样。

                  在这样的服务器架构中,处理任务的线程或子进程可以被视为可能失败的应用程序。上述所有注意事项都适用于这样的任务:错误应该通过干净的任务退出尽快浮出水面,并且应该记录足够的信息以允许容易地发现和修复错误。例如,当此类任务在 Java 中退出时,通常应记录导致退出的任何 RuntimeException 的整个堆栈跟踪。

                  尽可能多的代码应该在处理任务的线程或进程中执行,而不是在主服务器线程或进程中执行。这是因为主服务器线程或进程中的任何错误仍然会导致整个服务器宕机。最好将代码及其包含的错误推送到任务处理代码中,这样当错误出现时不会导致服务器崩溃。

                  正常操作中无法避免的预期错误

                  在正常操作中无法避免的预期错误,例如来自数据库或与应用程序分离的其他服务的异常,需要非常不同的处理方式。在这些情况下,目标不是修复代码,而是让代码在合理的情况下处理错误,并通知可以解决问题的用户或操作员。

                  在这些情况下,例如,应用程序可能希望丢弃迄今为止累积的所有结果,然后重试操作。在数据库访问中,使用事务有助于确保丢弃累积的数据。在其他情况下,在编写代码时考虑到此类重试可能会很有用。幂等性的概念在这里也很有用。

                  当自动重试无法充分解决问题时,应通知人工。应通知用户操作失败;通常可以为用户提供重试的选项。然后,用户可以判断是否需要重试,还可以对输入进行更改,这可能有助于重试时事情变得更好。

                  对于此类错误,可以使用日志记录和电子邮件通知来通知系统操作员。与记录编程错误不同,正常操作中预期的错误记录应该更简洁,因为错误可能发生多次,并且在日志中出现多次;操作员通常会分析许多错误的模式,而不是专注于单个错误。

                  库和应用程序

                  上面对错误类型的讨论直接适用于应用程序代码。错误处理的另一个主要上下文是库代码。库代码仍然有相同的两种基本类型的错误,但它通常不能或不应该直接与用户通信,并且它对应用程序上下文的了解比应用程序代码少,包括是否可以接受立即退出。

                  因此,库应如何处理日志记录、它们应如何处理正常操作中可能出现的错误以及它们应如何处理编程错误和其他不应该发生的错误方面存在差异。

                  关于日志记录,如果可能,该库应支持以客户端应用程序代码所需的格式进行日志记录。一种有效的方法是根本不进行日志记录,并允许应用程序代码根据库提供给应用程序代码的错误信息进行所有日志记录。另一种方法是使用可配置的日志接口,允许客户端应用程序提供日志记录的实现,例如在首次加载库时。例如,在 Java 中,该库可能会使用 logback 日志接口,并允许应用程序担心要配置哪些日志实现以供 logback 使用。

                  对于不应该发生的错误和其他错误,库仍然不能简单地退出应用程序,因为这可能是应用程序无法接受的。相反,库应该退出库调用,为调用者提供足够的信息来帮助诊断问题。该信息可以以带有堆栈跟踪的异常的形式提供,或者如果正在使用可配置的日志记录方法,则库可以记录该信息。然后,应用程序可以将其视为任何其他此类错误,通常通过退出或在服务器中,通过允许任务进程或线程退出,并使用与编程错误相同的日志记录或错误报告应用程序代码。

                  正常操作中预期的错误也应报告给客户端代码。在这种情况下,与在客户端代码中遇到这种类型的错误一样,与错误相关的信息可以更加简洁。通常库应该减少对此类错误的本地处理,更多地依赖客户端代码来决定是否重试以及重试多少次。然后,如果需要,客户端代码可以将重试决定传递给用户。

                  实际问题

                  既然我们已经掌握了哲学,让我们将其应用于您提到的实际问题。

                  • 如何决定错误是应该在本地处理还是传播到更高级别的代码?

                  如果这是正常操作中预期的错误,请重试或可能在本地咨询用户。否则,将其传播到更高级别的代码。

                  • 如何决定是记录错误还是向用户显示错误消息?

                  如果这是正常操作中预期的错误,并且用户输入有助于确定要采取的操作,则获取用户输入并记录简洁的消息;如果这似乎是一个编程错误,请向用户提供一个简短的通知并记录更多的信息。

                  • 日志记录是否应该只在应用程序代码中完成?或者可以从库代码中进行一些日志记录。

                  从库代码记录应该在客户端代码的控制之下。最多,该库应该记录到客户端为其提供实现的接口。

                  • 如果出现异常,一般应该在哪里捕获它们?在低级或高级代码中?

                  可以在本地捕获正常操作中预期的异常,然后重试操作或以其他方式处理。在所有其他情况下,应允许传播异常。

                  • 你是应该争取通过所有代码层的统一错误处理策略,还是尝试开发一个可以适应各种错误处理策略的系统(以便能够处理来自 3rd 方库的错误) .

                  第三方库中的错误类型与应用程序代码中出现的错误类型相同。错误应主要根据其所代表的错误类型进行处理,并对库代码进行相关调整。

                  • 创建错误代码列表是否有意义?还是现在已经过时了?

                  应用程序代码应在编程错误的情况下提供完整的错误描述,并在正常操作中可能出现的错误情况下提供简洁的描述;在任何一种情况下,描述通常比错误代码更合适。库可能会提供错误代码来描述错误是编程错误还是其他内部错误,或者该错误是否是在正常操作中可能发生的错误,后一种类型可能细分得更细;但是,在可能的情况下,异常层次结构可能比语言中的错误代码更有用。请注意,从命令行运行的应用程序可能会充当 shell 脚本的库。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2020-06-16
                    • 1970-01-01
                    • 2016-08-18
                    • 1970-01-01
                    • 2010-11-12
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多