Как отличить неожиданные (тупоголовые) исключения от ожидаемых (экзогенных)?

В этом посте я использую категоризацию исключений @Eric Lippert, которую вы можете найти здесь: Обидные исключения

Самые важные в этом случае:

Кропотливые исключения - это ваша чертова ошибка, вы могли их предотвратить, и поэтому они являются ошибками в вашем коде. Вы не должны их ловить; это скрывает ошибку в вашем коде. Скорее, вы должны написать свой код так, чтобы исключение не могло произойти в первую очередь, и, следовательно, его не нужно было перехватывать.

Экзогенные исключения выглядят чем-то вроде неприятных исключений, за исключением того, что они не являются результатом неудачного выбора дизайна. Скорее, они являются результатом неопрятной внешней реальности, посягающей на вашу красивую и четкую логику программы. Всегда обрабатывайте исключения, указывающие на неожиданные экзогенные условия; как правило, нецелесообразно и нецелесообразно предвидеть все возможные отказы. Просто попробуйте операцию и будьте готовы обработать исключение.

Как, наверное, испытал каждый разработчик, в программном обеспечении для крупного предприятия невозможно избежать упрямых исключений на 100%.

В неудачной ситуации, когда возникает тупое исключение, я хочу проинформировать пользователя, чтобы он сообщил нам об ошибке (поддержка третьего уровня). Кроме того, в этом случае я хочу записать сообщение с уровнем журнала «Ошибка».

Для экзогенных исключений я хочу показать пользователю более конкретное сообщение с некоторыми подсказками, потому что он, вероятно, сможет решить проблему сам (возможно, с помощью поддержки первого или второго уровня)

В настоящее время я добиваюсь этого путем явного перехвата ТОЛЬКО экзогенных исключений в компонентах низкого уровня и их упаковки в пользовательские исключения. На верхнем уровне (в моем случае ViewModel приложения MVVM WPF) я явно перехватываю настраиваемое исключение, чтобы показать предупреждение. Во втором блоке catch я перехватываю общие исключения, чтобы показать ошибку.

Является ли это общепринятым и передовым методом различения тупых исключений и экзогенных исключений в корпоративных приложениях? Есть ли лучший подход? Или это совсем не обязательно?

Прочитав эту статью dotnetpro - Implementierungsausnahmen, мне тоже интересно, если я должен обернуть все (также тупоголовые) исключения в пользовательские исключения, чтобы предоставить дополнительную информацию о контексте при их регистрации?

О переносе всех исключений я нашел следующие сообщения: stackoverflow - Должен ли я поймать и обернуть общее исключение ? и stackoverflow - Должен ли я улавливать все возможные конкретные исключения или просто общее исключение и оборачивать его в индивидуальное? Это выглядит довольно спорным и зависит от варианта использования, поэтому я не уверен в моем случае.

Пример обработчика catch высокого уровня в 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 [email protected]", 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);
        }*/
    }
}

person Jonas Benz    schedule 09.07.2019    source источник


Ответы (1)


Несмотря на то, что в нашем коде нет такой точной классификации, наша обработка исключений часто неявно указывает, возможно ли, по нашему мнению, конкретное исключение (экзогенное) или же мы просто учитываем возможные ошибки.

Используя пример Эрика, если мы обращаемся к файлу, помещаем его в try/catch и явно перехватываем FileNotFoundException, это должно указывать на то, что мы осознали, что FileNotFoundException был возможным результатом, даже если мы проверили миллисекунду ранее, чтобы убедиться, что это существует.

Если, с другой стороны, наш код включает это:

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

... это предполагает, что мы принимаем во внимание тупоголовое исключение, то, что могло произойти где угодно в коде, выполняемом в try.

Что (как бы) отличает их, так это то, что один указывает на то, что мы осознали, что это было возможно, и объяснили это, а другой говорит: «Будем надеяться, что здесь ничего не пойдет не так».

Даже это различие не совсем ясно. Наш код доступа к файлу может находиться в "неопределенном" try/catch(Exception ex) блоке. Мы знаем, что удаленно возможно, что из-за состояния гонки файл может не существовать. В этом случае мы просто позволим этой расплывчатой ​​обработке исключений уловить это. Это может зависеть от того, что должно произойти. Если мы удаляли файл и выясняется, что его не существует, нам не нужно ничего делать. Если нам нужно было его прочитать, а теперь его нет, это просто исключение. Улавливание этого конкретного исключения может оказаться бесполезным, если результат будет таким же, как и для любого другого исключения.

Точно так же то, что мы явно перехватываем исключение, не гарантирует, что оно не «тупоголовое». Может я что-то делаю не так, и иногда мой код выдает ObjectDisposedException. Понятия не имею, почему он это делает, поэтому добавляю catch(ObjectExposedException ex). На первый взгляд может показаться, что я знаю, что происходит в моем коде, но на самом деле это не так. Я должен был выяснить проблему и исправить ее, вместо того, чтобы перехватывать исключение, не имея понятия, почему это происходит. Если приложение время от времени не работает, и я не знаю почему, то факт, что я поймал исключение, в лучшем случае бесполезен, а в худшем - вреден, потому что он скрывает то, что на самом деле происходит.


Это не означает, что мы должны добавлять try/catch операторов в каждый метод для перехвата «тупых» исключений. Это просто пример обработки исключений, который учитывает возможность исключения, которое могло или не могло быть ошибкой. Обычно это полезно не для каждого метода. Мы могли бы поставить ровно столько по краям, чтобы гарантировать, что любое выброшенное исключение, по крайней мере, будет зарегистрировано.


Что касается перехвата и упаковки исключений в новые исключения, часто все сводится к тому, что вы планируете делать с этой дополнительной информацией, которую создаете. Часто ответ - ничего.

У нас мог бы быть один уровень приложения, которое генерирует всевозможные искусно обернутые пользовательские исключения. Затем другой слой вызывает его и делает следующее:

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

Что мы сделали с нашим необычным пользовательским исключением? Мы просто зарегистрировали его, как если бы мы не обернули его. Когда мы смотрим на наши журналы, мы игнорируем настраиваемое исключение и просто смотрим на трассировку стека исходного исключения. Он сообщает нам, из какой сборки, класса и метода возникло исключение. Вероятно, это все, что нам нужно.

Если целью обернутого исключения является добавление контекста, например

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

... это может быть полезно. Если бы мы этого не сделали, исключение составило бы «Последовательность не содержит элементов». Это не так ясно.

Но возможно ли, что мы получим от этого точно такой же пробег?

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

Если нигде нет строчки кода, в которой говорится, что catch(DataStoreLoadException ex) и в результате делает что-то другое, чем это было бы с любым другим исключением, то, скорее всего, мы не получаем от этого никакой пользы.

Стоит отметить, что много лет назад Microsoft рекомендовала, чтобы наши настраиваемые исключения наследовали от ApplicationException, а не от Exception. Это позволит провести различие между пользовательскими и системными исключениями. Но стало очевидно, что это различие не добавляет ценности. Мы не говорили: «Если это ApplicationException, делай то, иначе делай то». То же самое часто верно и для других настраиваемых исключений, которые мы определяем.

person Scott Hannen    schedule 09.07.2019
comment
Спасибо, что поделились своим мнением. В заключение: не думаете ли вы, что стоит стараться отличать тупоголовые исключения от экзогенных? (чтобы иметь возможность регистрировать тупые исключения на более высоком уровне журнала, например Error, и показывать пользователю сообщение о том, что он должен связаться со службой поддержки) Как в противном случае вы решите, какой уровень журнала использовать? Какое сообщение вы показываете пользователю? Если вы не поймаете конкретные исключения, как вы можете представить конкретные полезные сообщения? (как контактная поддержка, в случае тупого исключения) - person Jonas Benz; 16.07.2019
comment
По моему опыту, нет, это не добавляет ценности. Что-то работает не так, как ожидалось - по любой причине, неважно, - вы в конечном итоге просматриваете журнал. Вы его находите и делаете выбор. Можно было выяснить, почему SQL Server не отвечает. Это может быть решение проблемы в коде. Самая большая разница, которая всегда будет иметь значение, - это между чем-то и ничем. Если вы знаете, что это за исключение и где оно возникло, вы выиграете. Если ты найдешь его быстро, ты быстрее выиграешь. - person Scott Hannen; 16.07.2019
comment
Но почему SQL-сервер не отвечает - это то, что заказчик может исправить сам (в случае, если база данных является локальной на стороне клиента). Или, по крайней мере, поддержка первого или второго уровня может ему в этом помочь. Разве это не помогает получать меньше обращений в службу поддержки, которые остаются на нашем столе для разработчиков? (поддержка третьего уровня) - person Jonas Benz; 16.07.2019
comment
Это более полезное различие. Требуются ли действия со стороны пользователей вместо тупоголовых против экзогенных? Скорее всего, что-то изменится, если они попробуют еще раз через минуту? (Тупая ошибка может привести к тому, что SQL Server перестанет отвечать, так что это полезное различие?) Различие, которое имеет значение для пользователя, заключается не в том, допустили ли мы глупую ошибку, а в том, должны ли они повторить попытку сейчас, позже, позвонить кому-нибудь , или сделайте что-нибудь еще. И убедитесь, что информация, которую мы им даем, верна. Например, не говорите им звонить в службу поддержки, если они искали то, чего никогда не будет. - person Scott Hannen; 16.07.2019