Совершенный код: обработка ошибок в библиотеках

Программисты каждый день пользуются сторонними библиотеками в своих программах, например, http-клиентами или парсерами. Помимо выполнения основных функций, все эти библиотеки как-то обрабатывают возникающие ошибки. Причем чем больше в библиотеке побочных эффектов — сетевое взаимодействие, работа с файлами — тем больше внутри кода, отвечающего за ошибки, и тем он сложнее.
В этой статье разберем принципы, по которым строится обработка ошибок внутри библиотек. Это поможет отличать хорошие библиотеки от плохих. Вы сможете лучше строить взаимодействие с ними и даже проектировать свои собственные библиотеки.
Подписывайтесь на канал Кирилла Мокевнина в Telegram — чтобы узнать больше о программировании и профессиональном пути разработчика
Прежде чем начать, давайте разберемся с терминологией. В отличие от программы, библиотека не может использоваться напрямую, например, в терминале. Любая библиотека — это код на конкретном языке, который вызывается другим кодом на этом же языке. Говорят, что у библиотеки есть клиент. Клиент — тот, кто использует библиотеку:
В свою очередь, находящийся внутри библиотеки код называется библиотечным кодом. Это разделение довольно важно, так как у каждой из этих частей своя зона ответственности.
Сами библиотеки часто реализованы как функция, набор функций, класс, или, опять же, набор классов. Обработка ошибок в этом случае различаться не будет, поэтому для простоты все примеры ниже будут построены на функциях.
Содержание
Про ошибки
Что вообще считать ошибкой, а что нет? Представьте функцию, которая ищет символ внутри строки и не находит его. Является ли это ошибкой?
Такое поведение нормально для данной функции. Если значения нет, все нормально, функция все равно выполнила свою задачу и вернула что-то осмысленное.
А что насчет http запроса как в примере выше? Как вести себя функции axios.get
, которая не смогла загрузить указанную страницу? С точки зрения функции такая ситуация не является нормальной. Если функция не может выполнить свое основное предназначение, это ошибка. Именно об этих ошибках и пойдет речь. Ниже конкретные примеры того, как делать стоит и как не стоит при использовании библиотек и их проектировании.
Завершение процесса
Во всех языках программирования есть возможность досрочно остановить процесс операционной системы, в котором выполняется код. Обычно это делается с помощью функции, имеющие в названии слово exit
. Вызов этой функции останавливает программу целиком.
Есть ровно одна причина, по которой такой код недопустим ни в одной библиотеке: то, что является фатальной ошибкой для конкретной библиотеки, не является такой же ошибкой для всей программы. В самом худшем случае программа предупредит пользователя о неудачной попытке загрузить сайт и попросит попробовать снова. Подобное поведение невозможно было бы реализовать в случае использования библиотеки, которая останавливает выполнение целой программы.
Проще говоря, библиотека не может решать за программу, когда ей завершаться. Это не ее зона ответственности. Задача библиотеки — оповестить клиентский код об ошибке, а дальше уже не ее забота. Оповещать можно с помощью исключений:
Вопрос на самоконтроль. Можно ли написать тесты на библиотеку, которая выполняет остановку процесса?
Имитация успеха
Иногда разработчик пытается скрыть от клиентского кода любые или почти любые ошибки. Код в таком случае перехватывает все возможные ошибки (исключения) и всегда возвращает какой-то результат. Ниже гипотетический пример с функцией axios.get
. Как бы она выглядела в этом случае:
Самая главная проблема в таком решении: оно скрывает проблемы и делает невозможным или практически невозможным отлов ошибки снаружи, в клиентском коде:
Правильное решение — использовать исключения.
Подавление ошибок
Этот способ очень похож на предыдущий. Код с подавлением ошибки выглядит примерно так:
Что здесь происходит? Разработчик выводит сообщение об ошибке в консоль и возвращает наружу, например, null
. Такой код появляется тогда, когда программист еще не до конца осознал, что такое библиотека. Главный вопрос, который должен вызывать этот код: а как клиентский код узнает об ошибке? Ответ здесь — никак. Подобную библиотеку невозможно использовать правильно. Представьте, если бы так себя вел axios.get
:
Иногда ситуация еще хуже. Внутри библиотеки используется код, который порождает исключения, что правильно, а программист их перехватывает и подавляет.
Правильное решение — порождать исключения и не подавлять исключения.
Коды возврата
Само по себе это не является ошибкой, но во многих ситуациях коды возврата нежелательны. О том, почему исключения предпочтительнее в большинстве ошибочных ситуаций, можно почитать в шикарной статье на Хабре.
Вопрос на самоконтроль: как должна себя вести функция валидации в случае нахождения ошибок: выкидывать исключение или возвращать ошибки, например, как массив?
Исключения
Как правило, это самый адекватный способ работы с ошибками в большинстве языков. Однако есть другие языки с совершенно другими схемами работы. Если ошибка фатальная, то она либо уже является исключением, либо исключение нужно выбросить:
Kirill Mokevnin
5 лет назад