7 декабря 2023 г.

Опасайтесь вторичных исключений

К нам обратился клиент, который пожаловался на то, что EurekaLog генерирует отчёт об ошибке в ненужном месте. Фактически, у клиента было ожидаемое исключение, которое он хотел скрыть, показав вместо этого простое сообщение. Клиент любезно показал свой код:
try
  Query.Delete; // - здесь возбуждается исключение
except
  Query.Transaction.Rollback;
  ShowMessage('Sorry, could not delete the report');
  Exit;
end;
Что происходит? Неужели EurekaLog игнорирует код пользователя?

Если бы код клиента был написан вот так:
try
  Query.Delete; // - здесь возбуждается исключение
except
  Exit;
end;
то проблемы бы не было: Query.Delete возбуждает исключение, исключение ловится блоком except, который ничего не делает, и выполнение продолжается дальше, EurekaLog не реагирует (*).

Однако код клиента выглядит иначе: его блок except выполняет некоторые (сложные) действия по восстановлению от исключения.

В любом случае такие исключения называются handled ("обработанными"), потому что их обрабатывает код пользователя (клиента). Handled-исключение обрабатывается кодом очистки, который производит некоторые действия по восстановлению/откату, после чего продолжается выполнение программы, а само исключение удаляется.

Вообще, любой код очистки (будь это деструктор или код отката/восстановления) должен быть написан так, чтобы не возбуждать исключений. Мне кажется, что логика тут очевидна: если ваш обычный код возбуждает исключение, то у вас всегда найдётся возможный алгоритм действий на этот случай (так называемый, "план Б"). Например, если произошло исключение при открытии документа, то его нужно закрыть; если произошло исключение при доступе к принтеру, то задание печати нужно отменить. При ошибке открытия файла можно повторить попытку открытия после паузы или действий пользователя. И так далее.

Но что вы будете делать, если ошибка происходит в самом "плане Б"? Вы захотели отменить задание печати, а оно... не отменяется. И что теперь? Вы не сможете предложить никакого внятного алгоритма восстановления. Ведь вы не знаете, что произошло. Ваши переменные могут быть испорчены. Чёрт, да вы, может быть, даже блок памяти сейчас выделить не сможете. Единственный возможный чистый способ - это завершение работы приложения.

Вот почему код очистки обязан не возбуждать исключения. Потому что вы ничего не сможете сделать внятного с этими исключениями. Иными словами: код очистки должен обрабатывать все исключения, которые он знает как обрабатывать.

Но на практике всё может быть не так, как в идеальном мире.

В данном случае код клиента возбуждает второе исключение внутри Query.Transaction.Rollback. Это означает, что выполнение блока except прерывается, и выполнение переходит к обработчику исключений выше по стеку. Оказывается, что в коде клиента больше нет блоков try выше по стеку, поэтому управление передаётся на глобальный обработчик исключений, который вызывает EurekaLog для создания отчёта.

Исправление кода будет заключаться в том, чтобы всё же выполнить правило "код очистки не должен возбуждать исключения" ("код очистки должен обрабатывать известные ему исключения"). Например, следующим образом:
try
  Query.Delete; // - здесь возбуждается исключение
except
  try
    Query.Transaction.Rollback;
  except
    on EDatabaseError do; // - игнорировать ошибки работы с базой данных
  end;
  ShowMessage('Sorry, could not delete the report');
  Exit;
end;
В этом случае второе исключение будет обработано на месте и не будет всплывать в следующий обработчик исключений (будь это try блок или глобальный обработчик исключений). При этом важно писать правильный код - т.е. так, чтобы он не блокировал бы исключения, которые вы не знаете как обрабатывать. Например, следующий код будет неверным:
  try
    Query.Transaction.Rollback;
  except
    // - игнорировать ВСЕ ошибки
  end;
Он неверный, потому что вы не знаете как правильно обработать/откатить/восстановиться после, например, исключения EAccessViolation. Лучше всего написать код так, чтобы он учитывал максимально узкие условия на исключений - только те, которые вы точно знаете как обрабатывать. Например, если вам известен конкретный тип/код ошибки при отмене транзакции в вашем случае, то пишите так:
  try
    Query.Transaction.Rollback;
  except
    on E: ESomeSpecificDatabaseException do
      if E.ErrorCode = 1234 then
      begin
        // - игнорировать только одну конкретную ошибку
      end
      else
        raise;
  end;



Примечания:
  • (*) Если только в EurekaLog не включена опция Catch Handled Exceptions. Когда опция Catch Handled Exceptions включена, EurekaLog будет показывать отчёты об ошибках обо всех исключениях - даже если они были обработаны блоками except. Эта опция предназначена для локальной отладки, как помощь в поиске "плохого" кода. Её не следует использовать в релизе.
  • Если у вас нет необходимости проводить специальную чистку после исключения, но нужно заблокировать ожидаемое исключение, то вы можете проигнорировать его, либо пометить как "ожидаемое".
  • Если же вы не исправляете ваш код, чтобы ваш код восстановления/обработки не возбуждал исключений, то у вас будут возникать вторичные исключения. В EurekaLog есть набор опций, контролирующий поведение EurekaLog для таких исключений.

Комментариев нет :

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.