Обработка ошибок сложна, и ее усложняет богатая иерархия ошибок. Программы, которые успешно обрабатывают ошибки, как правило, имеют только пару универсальных обработчиков. Код, который перехватывает определенные типы ошибок во многих местах и ​​пытается дифференцировать их обработку, в конечном итоге работает неправильно. Как мы можем предоставить как подробную информацию об ошибках, так и упрощенную обработку?

Сколько типов ошибок нам нужно? Вроде всего 2, а может и 3.

Информация об ошибке и серьезность

Когда происходит ошибка, мы по праву хотим собрать как можно больше информации о ней. Был ли аргумент вне допустимого диапазона, не мог ли файл быть загружен, была ли сеть отключена или есть что-то, на что стоит обратить внимание? Эта информация важна как для отладки, так и для предоставления пользователю осмысленных сообщений об ошибках. Но эта информация — одна из причин, по которой мы получили раздутую иерархию ошибок.

Но эта расширенная информация ортогональна типу ошибки. Обработчик заботится только о серьезности, а не о деталях.

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

В среде с богатой иерархией исключений, такой как Java, C# и даже большинство C++, единственной полезной обработкой является перехват всех исключений!

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

Ошибки тегирования

Есть решение информационной проблемы. Впервые я увидел его в библиотеке исключений Boost для C++. Вместо того, чтобы создавать бесконечное количество типов исключений, он использует механизм тегов. Мы можем добавлять произвольные детали к любому исключению, не меняя его тип.

Этот код использует довольно общий recoverable_error. Он добавляет tag_reason, говорящий о том, что пошло не так, и tag_input_data, ссылающийся на исходные данные. Мы создали подробную ошибку без изменения типа исключения.

В этом подходе также нет необходимости делать обертку. Обработчики могут добавлять дополнительную информацию напрямую.

Мы сохранили тот же тип recoverable_error и добавили к нему tag_filename. Теперь эта ошибка имеет tag_reason, tag_input_data и tag_filename. Мы напечатаем их все в журнале.

Я считаю ошибки с тегами надежным решением, но не встречал их, кроме использования исключений Boost в C++.

Уровни серьезности

Если нам не нужны новые типы для переноса деталей, то какие типы ошибок нам нужны?

Я долго спорил об этом с несколькими коллегами. Мы до сих пор не согласны с тем, два это типа или три. Да, это шокирующе низкие цифры!

Один из этих типов ошибок легко поддерживать: критическая ошибка. Это ситуации, с которыми невозможно справиться правильно. Возможно, когда серьезный сбой существенно сломал систему, например, обнаружение повреждения памяти, невозможность выделить небольшой объект или нарушение безопасности виртуальной машины. Многие библиотеки C вызывают abort при таких ошибках. Мы можем распространять их как обычно, но только для получения дополнительной информации для отладки — их нельзя восстановить!

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

Два типа: без гражданства и с состоянием

Я за два вида:

  • Ошибки без сохранения состояния/развертывания: это такие вещи, как проверка аргументов и предварительная проверка. Они происходят до изменения какого-либо состояния в системе. Состояние вызывающей стороны будет точно таким, каким оно было до неудачного вызова функции.
  • Ошибки с сохранением состояния/устранимые: они происходят после того, как что-то уже было изменено. Вызывающий должен предположить, что используемые им объекты, участвующие в вызове, находятся в другом состоянии и должны быть очищены.

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

Функциональное программирование предлагает другой взгляд на эти две ошибки. Если мы используем только чистые функции, у нас не может быть класса ошибок с отслеживанием состояния. К сожалению, программу нельзя построить только из одних только функций, но об этом стоит помнить.

Аргумент против двухтипного подхода звучит примерно так: программисты просто все испортят.

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

Более чем вероятно, что программист будет осторожен и поднимет только тип «с сохранением состояния/восстановление», так как он не уверен, что ничего не изменил. Вызвать ошибку с сохранением состояния всегда безопасно, но неправильное генерирование ошибки без сохранения состояния может привести к катастрофе.

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

Такой подход не мог бы работать, если бы компилятор не выполнял большую часть работы.

В листе

Но я пишу компилятор, так что, возможно, идея сработает. Может ли компилятор решить, какой тип ошибки вызвать, и выполнить необходимые эскалации? Кажется, это должно быть возможно. Компилятор знает обо всей задействованной памяти и всех выполненных назначениях. Конечно, он может определить, является ли ошибка без сохранения состояния или с отслеживанием состояния.

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

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

Первоначально опубликовано на сайте mortoray.com 13 июня 2017 г.