Генерация исключений это чистое зло
7 августа 2025

Генерация исключений. Вы конечно знаете что это такое и точно использовали, если хотя бы немного программировали.
Вот вам новость: генерация исключений – это очень часто плохо.
В любой ситуации когда блок 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 раз.
Я просто оставлю это здесь:
A
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
?