如何区分意外的(愚蠢的)异常和预期的(外生的)异常?

How to distinguish unexpected (boneheaded) exceptions from expected (exogenous) ones?

在这个 post 中,我使用了@Eric Lippert 的异常分类,您可以在这里找到它: Vexing exceptions

本例中最重要的:

Boneheaded exceptions are your own darn fault, you could have prevented them and therefore they are bugs in your code. You should not catch them; doing so is hiding a bug in your code. Rather, you should write your code so that the exception cannot possibly happen in the first place, and therefore does not need to be caught.

Exogenous exceptions appear to be somewhat like vexing exceptions except that they are not the result of unfortunate design choices. Rather, they are the result of untidy external realities impinging upon your beautiful, crisp program logic. Always handle exceptions that indicate unexpected exogenous conditions; generally it is not worthwhile or practical to anticipate every possible failure. Just try the operation and be prepared to handle the exception.

就像每个开发人员可能都经历过的那样,在大型企业软件中不可能 100% 避免愚蠢的异常

在抛出愚蠢的异常的不幸情况下,我想通知用户,以便他将错误报告给我们(三级支持)。此外,在这种情况下,我想记录一条日志级别为 "Error" 的消息。

对于外部异常,我想向用户显示更具体的消息和一些提示,因为他可能可以自己解决问题(也许在一级或二级支持的帮助下)

我目前实现这一点的方法是仅在低级组件中显式捕获 外生异常 并将它们包装到自定义异常中。然后在顶层(在我的例子中是 MVVM WPF 应用程序的 ViewModel) 显式捕获自定义异常,以显示警告。在第二个 catch 块中,我捕获了一般异常以显示错误。

这是区分企业应用程序中的愚蠢异常和外生异常的常见良好做法吗?有更好的方法吗?还是完全没有必要?

看完这篇文章后dotnetpro - Implementierungs­ausnahmen我也想知道,我是否应该将所有(也是愚蠢的)异常包装到自定义异常中以在记录它们时提供更多上下文信息?

关于包装我在 post 之后发现的所有异常:Whosebug - Should I catch and wrap general Exception? and Whosebug - Should I catch all possible specific exceptions or just general Exception and wrap it in custom one? 它似乎很有争议并且取决于用例,所以我不确定我的情况。

ViewModel 中的高级捕获处理程序示例:

public class MainWindowViewModel
{
    private readonly ICustomerRepository _customerRepository;

    public MainWindowViewModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
        PromoteCustomerCommand = new DelegateCommand(PromoteCustomer);
    }

    public ICommand PromoteCustomerCommand { get; }

    private void PromoteCustomer()
    {
        try
        {
            Customer customer = _customerRepository.GetById(1);
            customer.Promote();
        }
        catch (DataStoreLoadException ex)
        {
            // A expected exogenous exception. Show a localized message with some hints and log as warning.
            Log(LogLevel.Warning, ex);
            ShowMessage("Unable to promote customer. It could not be loaded. Try to...", ex);
        }
        catch (Exception ex)
        {
            // A unexpected boneheaded exception. Show a localized message, so that the users contacts the support and log as error.
            Log(LogLevel.Error, ex);
            ShowMessage("Unable to promote customer because of an unknown error. Please contact support@example.com", ex);
        }
    }
}

低级异常包装示例:

public class SqlCustomerRepository : ICustomerRepository
{
    public Customer GetById(long id)
    {
        try
        {
            return GetFromDatabase(id);
        }
        catch (SqlException ex)
        {
            // Wrap the exogenous SqlException in a custom exception. The caller of ICustomerRepository should not depend on any implementation details, like that the data is stored in a SQL database.
            throw new DataStoreLoadException($"Unable to get the customer with id {id} from SQL database.", ex);
        }

        // All other exceptions bubble up the stack without beeing wrapped. Is it a good idea, or should I do something like this to provide additional context? (Like the id in this case)
        /*catch (Exception ex)
        {
            throw new DataStoreException($"Unknown error while loading customer with id {id} from SQL database.", ex);
        }*/
    }
}

虽然我们的代码中没有 class非常精确的化,但我们的异常处理通常隐含地表明我们是否认为特定的异常是可能的(外生的),或者我们是否只是在考虑可能的异常错误。

使用 Eric 的示例,如果我们访问一个文件,将其放在 try/catch 中,并显式捕获 FileNotFoundException,那么 应该 表明我们实现了FileNotFoundException 是一个可能的结果,即使我们提前一毫秒检查它是否存在。

另一方面,如果我们的代码包含以下内容:

try
{
    // do some stuff
}
catch(Exception ex)
{
    // maybe log it
}

...这个 建议 我们正在考虑愚蠢的异常,这可能发生在 try 中执行的代码中的任何地方。

他们(某种程度上)的区别在于,一个表明我们意识到这是可能的并解释了它,而另一个则说,"Let's hope nothing goes wrong here."

即使是这种区别也不是很清楚。我们的文件访问代码可能在 "vague" try/catch(Exception ex) 块中。我们知道,由于竞争条件,文件可能不存在的可能性很小。在那种情况下,我们只会让模糊的异常处理捕获它。这可能取决于需要发生什么。如果我们要删除该文件,但发现它不存在,则我们不需要做任何事情。如果我们需要阅读它而现在它不见了,那只是一个例外。如果结果与任何其他异常相同,捕获该特定异常可能对我们没有任何好处。

同样,仅仅因为我们明确地捕获异常并不能保证它不是 "boneheaded." 也许我做错了什么,有时我的代码会抛出 ObjectDisposedException。我不知道为什么会这样,所以我添加了 catch(ObjectExposedException ex)。乍一看,我似乎知道我的代码中发生了什么,但我真的不知道。我应该找出问题并修复它,而不是在不知道为什么会发生的情况下捕获异常。如果应用程序偶尔不工作并且我不知道为什么,我捕获异常的事实充其量是无用的或最坏的是有害的,因为它隐藏了真正发生的事情。


这并不是说我们应该在每个方法中添加 try/catch 语句来捕获 "boneheaded" 异常。这只是异常处理的一个例子,它说明了可能是错误也可能不是错误的异常的可能性。在每种方法中都这样做通常没有用。我们可能会在边缘放置足够多的内容,以确保抛出的任何异常至少会被记录下来。


至于在新异常中捕获和包装异常,通常归结为您打算如何处理您正在创建的额外信息。通常答案是 nothing.

我们可以让应用程序的一层抛出各种巧妙包装的自定义异常。然后另一层调用它并执行此操作:

try
{
    _otherLayer.DoSomeStuff();
}
catch(Exception ex)
{
    _logger.Log(ex);       
}

我们对花哨的自定义异常做了什么?我们只是记录了它,就像我们 没有 包装它一样。当我们查看日志时,我们会忽略自定义异常,只查看原始异常的堆栈跟踪。它告诉我们异常来自什么程序集、class 和方法。这可能就是我们所需要的。

如果包装异常的目的是添加上下文,比如

throw new DataStoreLoadException($"Unable to get the customer with id {id} from SQL database.", ex);

...这可能会有用。如果我们不这样做,那么例外情况将是 "The sequence contains no elements." 这还不清楚。

但我们是否有可能从中获得完全相同的里程数?

throw new Exception($"Unable to get the customer with id {id} from SQL database.", ex);

如果在任何地方都没有一行代码表明 catch(DataStoreLoadException ex) 并且 执行的结果与任何其他异常 不同,那么我们很可能我没有从中受益。

值得注意的是,多年前微软建议我们的自定义异常继承自 ApplicationException 而不是 Exception。这将区分自定义异常和系统异常。但很明显,这种区别没有增加任何价值。我们并不是说,"If it's an ApplicationException do this, otherwise do that." 我们定义的其他自定义异常通常也是如此。