Генерация исключений это чистое зло

7 августа 2025

Генерация исключений это чистое зло hero image

Генерация исключений. Вы конечно знаете что это такое и точно использовали, если хотя бы немного программировали.

Вот вам новость: генерация исключений – это очень часто плохо.

В любой ситуации когда блок try/except (или try/catch) используется не для “упаковки” сайд эффектов вроде чтения файлов, похода в сеть или в изолированном месте вокруг функции (в целях точечного дебага или просто потому что “ну никак нельзя иначе”) – от него лучше избавиться.

Но почему? Потому что такие try/execept сильно запутывают два “процесса”:


Подумайте сами – любая инструкция в блоке try/except может бросить исключение.

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

Современные статические анализаторы испытывают… некоторые сложности с “невидимыми путями исполнения”.


Короткая историческая справка.

Про исключения заговорили еще очень давно — в 40-50 годах. Первая программная реализация появилась в LISP, там было две функции — ERROR, которая вызывалась в случае программного сбоя, и ERRORSET, которая возвращала, внимание — либо код ошибки, либо NIL.

Иными словами, эта история в LISP вообще не про те исключения, которые мы имеем сейчас… Концепция и реализация развивалась, развивалась, и доразвивалась до того, что вместо кодов ошибок мы начали генерировать исключения, а именно — создавать условия исключений и обрабатывать их по отдельности.

Период массового распространения генераций исключений пришелся на 80-90 годы.

И вот он результат. Из исследования Carnegie Mellon за 1999 год:

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

Я пишу эту статью в 2025 году, и структурированного, надёжного, реализованного метода обработки исключений всё ещё нет.

Мейнстрим ООП-языки предоставляют всё тот же try/catch, а фреймворки ровным слоем размазывают сверху несколько слоёв абстракций, ещё больше размывая error boundaries, часто не предоставляя разработчикам никаких способов обработки ошибок, кроме как… завернуть всё это ещё в один try/catch.

В довесок генерация исключений может очень сильно тормозить работу. Вот занимательное чтиво из мира Java. Впрочем, одной джавой не ограничивается — тут статья про эксперименты на C++. В Python, особенно с 3.11, ситуация вроде бы не такая драматичная, возникающие исключения замедляют код всего-то в 3-5 раз.

Я просто оставлю это здесь:

try/except block is extremely efficient if no exceptions are raised.


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

Программы текли, разрабатывались естественным образом “сверху-вниз”. Благодаря такой детерминированности их было проще читать и людям, и машинам — первый статический анализатор кода lint был публично выпущен в 1979 году. Ну а дальше вы уже знаете.

Что же нам со всем этим делать?

Можно поступить как Google (особенно если вы пишете на плюсах):

We do not use C++ exceptions.

На любом другом популярном объектно-ориентированном языке нужно… прекратить использовать исключения :)

Ну или хотя бы не зарывать их глубоко в код — пусть блок try/catch будет оборачивать функцию прямо там, где она вызывается (снаружи или внутри). Да, это может добавить некоторое количество boilerplate-кода, но зато при возникновении исключений поток выполнения будет быстрее возвращаться на детерминированно протоптанную “дорожку”.

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

А ещё можно вспомнить Golang. Какой бы он угловатый и dumbed down ни был, самое большое дизайн-решение, направленное на укрепление Go-кода, выдаваемого разработчиками, — это заставить их обрабатывать каждую ошибку явно. Да, в Go есть defer/panic/recover, но придётся потрудиться, чтобы извернуть, исковеркать поток выполнения сильно.

Ну и наконец — функциональное программирование. Result-монада, например. Занавес. ¯\_(ツ)_/¯

Впрочем, я думаю, большинству разработчиков будет гораздо проще просто отказаться от утекающих raise/throw — посмотрите на свой try/except внимательно — может быть, тут можно if? Посмотрите на raise — как насчёт return?