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

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

Давайте рассмотрим пример, в котором применено защищённое от ошибок целочисленное деление:

int safeDiv(int a, int b) {
   if (b == 0)
      throw Div0(); // Исключения передаются особым образом
   return a / b; // Теперь-то всё абсолютно безопасно, ведь так?
}

Новые языки программирования склонны применять сообщения об ошибках в функциональном стиле и кодировать ошибки в возвращаемый тип. Например, Go кодирует ошибку в возвращаемый тип при помощи кортежа (res, err), а Rust возвращает Result<T, E> — тип-сумму результата и ошибки.

Даже в более старых языках наподобие C++ значения ошибок сегодня включают в стандартную библиотеку при помощи std::expected:

// Ошибка стала частью сигнатуры функции 
std::expected<int, Div0> safeDiv(int a, int b) {
   if (b == 0)
      return std::unexpected(Div0());
   return a / b;
}

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

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

На самом деле, чаще всего всё происходит наоборот.

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

▍ Так было ли использование типов результатов ошибкой?


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

Кроме того, они обеспечивают более высокую производительность; в рассмотренном ниже примере реализация на C++ с использованием исключений примерно в четыре раза быстрее, чем на Rust.

▍ С исключениями гораздо проще работать


Стоит учитывать, что я пишу эту статью с точки зрения человека, работающего с серверным кодом, значительный объём которого хранится в памяти. Это значит, что я отдаю приоритет выполнению серверного процесса, и скорее смирюсь со сбоем одного запроса, чем с вылетом всего приложения. Чтобы соблюдать SLA аптайма, мне нужно правильно обрабатывать любые сбои. Да, даже те, при которых не справляется абстрактная машина языков программирования и начинают проявляться ограничения реальной системы. Память небесконечна, CPU не могут обрабатывать все целые числа, а Деда Мороза не существует. Хоть моя точка зрения определённо не полностью объективна, я считаю, что от подобного подхода могут выиграть и другие системы. Даже в случае фронтенд-приложений я бы предпочёл увидеть диалоговое окно ошибки, чем вылет и потерю состояния.

В случае серверных программ у нас есть хороший способ обработки ошибок: отправка сообщения об ошибке клиенту, а если это не срабатывает, то закрытие соединения. Да, даже в ситуациях out-of-memory и SIGFPE, которое возникает при делении на ноль, можно отправлять сообщения об ошибках, а не создавать панику для всей системы. Да, закрытие всей программы — это «безопасно», но не особо продуктивно и к тому же может представлять собой вектор для DoS-атак.

Исключения чрезвычайно всё упрощают. Где бы мы ни находились, пусть даже на глубине в сотню стеков вызовов в далёком уголке программы, при обнаружении ошибки мы выбрасываем исключение. После этого всю сложную работу берёт на себя раскрутка стека. В Linux это обычно происходит в libgcc, определяющей способ раскрутки при помощи двоичного файла с DWARF eh_frame. Красота раскрутки заключается в том, что вызываются все деструкторы, удаляются все дескрипторы ресурсов, а все удерживаемые блокировки снимаются. А лучше всего то, что вся магия происходит за кулисами, ведь компилятор и так генерирует правильную информацию о раскрутке. [Если, конечно, вы не занимаетесь реализацией JIT и написанием компилятора, но это уже тема для отдельного поста.]

Так что простого блока try-catch в начале нашей обработки соединения будет достаточно для правильной обработки ошибки и передачи сообщения клиенту.

▍ Бойлерплейт


Сравним это с ошибками в функциональном стиле, когда обработка ошибок выполняется вручную и очень утомительно. Необходимо явным образом проверять, является ли значение возврата ошибкой, и пересылать его. Нужно снова и снова писать бойлерплейтные if (err) return err, которые замусоривают код.

Давайте вкратце исследуем метрики кода двух примерно одинаковых проектов баз данных. CockroachDB — проект на Go с явно обрабатываемыми ошибками:

$ rg 'if err != nil' | wc -l
24570

Почти 25 тысяч путей обработки ошибок!

Большинство этих ошибок — это артефакты повторяющегося стиля отчётности с пересылкой, но есть и достаточно много случаев, просто используемых для log.Fatal(). На мой взгляд, это не особо удобно.

CedarDB — наш проект, в котором используются исключения C++:

$ rg 'catch \(' | wc -l
140

В сто с лишним раз меньше кода!

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

Разумеется, существуют решения, позволяющие снизить объём бойлерплейта. В Rust можно подсластить некрасивый синтаксис макросом ?. Он скроет все повторяющиеся проверки, но всю работу вам всё равно придётся делать. Кроме того, теперь ваша функция возвращает коды ошибок, то есть вам придётся изменить публичный интерфейс и рекурсивно менять все функции, вызывающие эту функцию. Вместо того, чтобы работать в своём уголке, ваша дополнительная проверка теперь распространяется на половину кодовой базы. С другой стороны, всегда есть искушение добавить немного unwrap(). Вас эта змея точно не укусит.

▍ Системные ошибки


Что, если я скажу вам, что большинство программ уже обрабатывает гораздо меньше ошибок, чем следует? Вернитесь к примеру с safeDiv из начала статьи и попробуйте поделить -232 на -1. Получится -232, но это число не умещается в диапазоне int. Ой-ёй.

Как же гарантировать, что ваши программы не вылетят? Добавлять возврат ошибки каждый раз, когда функция делает assert? Но и это ещё не всё: распределение памяти может сбоить, стек и арифметические могут переполняться. Без выбрасывания исключений такие состояния отказа часто просто оказываются незамеченными. Вызываете, казалось бы, невинную функцию? Переполнение стека! Ой, ваш сервер свалился. Складываете два значения? Паника в Rust! Присваиваете значение временному объекту? Поздоровайтесь с OOM killer! В любой части потока кода, которая управляется пользователем (а чем он не управляет?), могут возникнуть подобные ошибки.

Что касается распределения памяти, если вы работает с Go, то вам не повезло, ведь у него есть сборщик мусора. На Rust у меня больше надежды, ведь работа по интеграции с ядром постепенно заставляет его расти и создавать более надёжные интерфейсы, не приводящие к панике, например, Box::try_new(). Но это потребует рефакторинга всего написанного кода.

Однако меня всё равно поражает то, что это должно считаться вершиной системного программирования. Даже Java справляется с этим гораздо лучше: перехватывает OutOfMemoryError и извиняется перед клиентом вместо того, чтобы убивать сервер целиком и прерывать обслуживание тысяч клиентов. Исключения позволяют удобно обрабатывать все эти системные ошибки. А в вашем собственном коде вам не придётся заново рефакторить мир заново каждый раз, когда вы обнаруживаете новый пограничный случай и добавляете проверку на ошибки в одну из частей кода.

▍ Исключения позволяют создавать более качественные сообщения об ошибках


Можно подумать, что при более явной обработке ошибок ближе к источнику ошибок мы наверняка будем получать более качественные сообщения об ошибках. Но мой опыт снова говорит об обратном: значения возврата при ошибках часто содержат мало информации и приводят к созданию плохих сообщений об ошибках. Классический пример — это системные вызовы, обычно отвечающие стандартам C. Возвращаемое значение часто оказывается просто кодом ошибки -1, а настоящая причина по каким-то причинам передаётся как побочный канал через errno. Разобраться в истинной причине сбоев часто невозможно по одному лишь коду ошибки, для этого требуется существенный объём контекста вокруг вызывающего кода. Вы получили EINVAL. Что делать дальше? strerror сообщает: «Invalid argument». Замечательно! Вероятно, система точно знает, что пошло не так, но отказывается сообщать подробности.

То есть errno сам по себе не особо полезен. Для получения нужных сообщений об ошибках вам нужен системный вызов в качестве контекста, но даже в этом случае ошибки остаются непонятными. Возьмём для примера write(2), который может возвращать EINVAL в случае, когда «object which is unsuitable for writing» или в случае невыровненных буферов. Но как определить, какой из этих двух вариантов верен?

Новые языки справляются с этим немного лучше, но ошибки Rust тоже попадают в ловушку error kind. Мы парсим где-нибудь int, и пользователю выводится IntErrorKind::InvalidDigit. Я тут выполняю парсинг многомегабайтных CSV, а вы говорите мне, что «в строке найдена недопустимая цифра». Меня это должно устроить? Разумеется, есть крейты вне std наподобие anyhow, которые делают это лучше. Но по моему опыту, значения возврата из-за ошибок только мотивируют к появлению плохих ошибок. Для создания правильного сообщения об ошибке необходимо иметь существенный объём контекста на пути возникновения ошибки, который не поместится в простой код ошибки. Вместо этого вам, вероятно, придётся распределять динамическую ошибку, как и в случае с исключениями. Для отладки может также понадобиться добавление обратной трассировки. И так вы получите исключения, только с лишними шагами!

▍ Исключения обеспечивают более высокую производительность


У исключений есть и ещё один туз в рукаве: при успешном выполнении они никак не снижают производительность, ведь мы разделяем поток управления. Значения возврата из-за ошибок перемешивают ошибку и путь успешного выполнения и всегда требуют проверок и ветвлений для обработки ошибок. За это приходится платить снижением производительности для каждого результата. Давайте рассмотрим пример с рекурсивным вычислением чисел Фибоначчи. Чтобы избежать переполнений, мы будем сообщать об ошибке при большой глубине вычислений.

В примерах ниже используется код на C++, что позволяет точнее сравнить две версии:

struct invalid_value { std::string reason; };

unsigned do_fib_throws(unsigned n, unsigned max_depth) {
   if (!max_depth) throw invalid_value(std::to_string(n) + " exceeds max_depth");
   if (n <= 2) return 1;
   return do_fib_throws(n - 2, max_depth - 1) + do_fib_throws(n - 1, max_depth - 1);
}

std::expected<unsigned, invalid_value> do_fib_expected(unsigned n, unsigned max_depth) {
   if (!max_depth) return std::unexpected<invalid_value>(std::to_string(n) + " exceeds max_depth");
   if (n <= 2) return 1;
   auto n2 = do_fib_expected(n - 2, max_depth - 1);
   if (!n2) return std::unexpected(n2.error());
   auto n1 = do_fib_expected(n - 1, max_depth - 1);
   if (!n1) return std::unexpected(n1.error());
   return *n1 + *n2;
}

Версия с expected чуть длиннее, но, по сути, это ведь один и тот же код, правда? Значит, и выполняться он должен одинаково!

Сравнение производительности исключений и значений ошибок.

С исключениями C++ десять тысяч итераций с n=15 выполняются за 7,7 мс. Со значениями возврата std::expected они выполняются за 37 мс — почти пятикратный рост времени исполнения! Можете убедиться сами: Quick Bench. Версия на Rust чуть быстрее, чем версия на C++ с expected, но всё равно в четыре раза медленнее, чем версия, выбрасывающая исключения! Даже если мы пожертвуем ради производительности строкой сообщения об ошибке и будем возвращать в expected только код ошибки, исключения всё равно вдвое быстрее. Да, разумеется, функции обычно бывают чуть больше (если вы не строго следуете стилю чистого кода; тогда вас, вероятно, не заботит производительность). Но в реальном коде я тоже наблюдал существенное замедление.

▍ Почему возвращаемые значения ошибок настолько медленнее?


Очевидная причина, то есть проверки на ошибки и ветвление, ничего не объясняет: следует ожидать, что CPU быстро научится прогнозировать ветвления, то есть, по сути, они будут «бесплатными», ведь так? Это прячет от нас тот факт, что эти проверки втихомолку потребляют обычно невидимые ресурсы CPU: кэш команд, кэш микроопераций, буфер истории ветвлений, буфер переупорядочивания и так далее. Так как выполнение почти любой функции может закончиться сбоем (помните, что распределения и математика могут и будут завершаться сбоем), проверка всевозможных ошибок занимает приличное количество ресурсов, которые нельзя использовать под полезный код. Исключения обрабатываются по совершенно иному пути исполнения кода, обычно выделенному в «холодных» частях, что уже даёт им большое преимущество.

Ещё одно отличие заключается в том, что вместо возврата одного int мы возвращаем «толстый» объект результата. Это делает вызов гораздо более затратным, потому что теперь нам нужно перемещать значения в стеке, что требует дополнительной подготовки. В случае успеха нам также нужно преобразовывать «толстый» результат в реальное значение. Это может помешать компиляторным оптимизациям, в частности, оптимизации хвостовых вызовов. В общем случае, пересылка ошибок в функциональном стиле требует больше регистров и места в стеке, что в свою очередь приводит к снижению степени встраивания кода. Разумеется, можно обойти эти проблемы, распределяя ошибки где-нибудь на странице, например, в локальной памяти потока. И именно так поступают исключения!

▍ Выбрасывание исключений когда-то было ужасно медленным


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

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

Однако раскрутка стека раньше обладала серьёзной проблемой: она синхронизировалась глобально. Чтобы найти местоположения кадра стека в двоичном файле, нам нужно смотреть в глобальную таблицу символов, которая затем используется для поиска информации о раскрутке для текущей функции. Как уже говорилось, эти символы находятся в разделе .eh_frame формата DWARF. Эта информация обычно статична, генерируется во время компиляции и обычно не требует синхронизации. Однако программы также могут загружать код динамически при помощи dlopen, например, для загрузки плагинов или для JIT-компиляции.

Проблема здесь заключается в том, что нам нужна изменяемая структура данных, в которой можно динамически обновлять информацию раскрутки для нового кода. Для защиты такой структуры libgcc просто создаёт мьютекс вокруг этого кода. Даже на машине с сотней ядер ни одно исключение не может быть выброшено одновременно. При высокой concurrency и множестве одновременных клиентов это может стать узким местом. К счастью, теперь в libgcc есть функциональность, позволяющая выполнять весь процесс раскрутки без блокировок. В простых случаях, например, когда не загружаются динамические кадры стека, блокировки использовать не нужно. В случае с динамическими кадрами всё чуть сложнее, но Томас Нойманн закоммитил реализацию B-дерева с оптимистичным связыванием блокировок для таких динамических блоков кода.

Однако даже при всём при этом раскрутка всё равно затратнее, чем должна быть. Для раскрутки стека таблицы eh_frame DWARF, по сути, интерпретируются, то есть здесь можно ещё многое улучшить. Уже существуют исследовательские предложения, например, Reliable and Fast DWARF-Based Stack Unwinding для компиляции таблиц раскрутки в нативный код, что должно быть на порядок величин быстрее, чем современные реализации исключений.

▍ В заключение


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

  • Исключения обеспечивают разделение задач, отделяя ошибочный путь.
  • Результаты при использовании кодов ошибок могут скрывать ошибки системного уровня, например, out-of-memory или переполнения.
  • По умолчанию благодаря исключениям можно легко предоставить контекст первопричины.
  • Код, использующий исключения, может работать быстрее, чем код со встроенными возвратами.

Я имею большой опыт работы с языком C++, позволяющим практически что угодно, поэтому мне непонятно, почему более новые языки наподобие Rust или Go не позволяют применять исключения. Хотя исключения можно использовать неверно, значения ошибок в функциональном стиле определённо не являются универсальным решением. Если вам необходимо определить контекст сообщения об ошибке, вам нужны исключения, так что у вас должна быть возможность их применять.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (55)


  1. rsashka
    16.09.2024 13:20

    ... склонны применять сообщения об ошибках в функциональном стиле и кодировать ошибки в возвращаемый тип
    ... почему более новые языки наподобие Rust или Go не позволяют применять исключения.

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

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


  1. AndreyDmitriev
    16.09.2024 13:20

    Исключения обеспечивают более высокую производительность

    Вот с этим утверждением я не совсем согласен, поскольку это зависит от многих факторов, хотя синтетический Фибоначчи и показывает ускорение. Пару лет назад я расширял библиотеку-wrapper для использования OpenCV в LabIVEW, где изначально обработка ошибок (размер картинок, тип, нулевой указатель и т.п.) была сделана как раз на исключениях. Это было красиво с точки зрения архитектуры, удобно и элегантно, но отжирало несколько процентов от общей производительности, а там где это использовалось был реалтайм и потоковое видео с детектора, мне была важна каждая миллисекунда. Я попробовал пооптимизировать всяко разно, но по итогу мне пришлось перейти к "Си-стилю" обработки ошибок на кодах возврата. К сожалению у меня под рукой нет кода, чтоб продемонстрировать. Это разумеется не значит, что утверждение абсолютно неверно, но это повод в случаях необходимости по крайней мере практически проверить и сделать трассировку и бенчмарк.


  1. MountainGoat
    16.09.2024 13:20
    +17

    Суть статьи кратко: почему делать плохо лучше, чем делать очень плохо. Почему не сделать нормально - умалчивается.

    Дано: заголовок функции на С++ или Питон. Требуется: не читая весь код функции, всех функций, что она вызывает рекурсивно, и всех функций стандартной библиотеки, что в них вызываются - составить список всех исключений, которые в процессе могут выпасть.
    Никак. Не возможно это. У джентльменов верят на слово документации, а программно перепроверить может только ИИ.

    По мне, это куда больший пипец, чем проблема цветных функций, с которой все носятся сейчас. Открывая файл на Питоне, остаётся просто надеяться, что все исключения, которые при этом могут вылететь, наследуются от OSError ( срочно в номер: НЕТ), чтобы можно было их перехватить.

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

    Мсье не знает ?, expect(), anyhow Рекомендую мсье дочитать хотя бы базовый учебник Rustbook до конца, потом уже говорить про Rust.

    Итого: считаю исключения в той же группе, что и NULL - они ни в коем случае не ошибка, а ошибкой было забирать их из С++ в более развитые языки.


    1. degistration
      16.09.2024 13:20

      я после заголовка на деда морозе

      Память небесконечна, CPU не могут обрабатывать все целые числа, а Деда Мороза не существует. Хоть моя точка зрения определённо не полностью объективна, я считаю, что от подобного подхода могут выиграть и другие системы. 

      подумал pro реальное использование метода исключения


      1. MountainGoat
        16.09.2024 13:20
        +2

        Но всегда есть нюанс...


    1. sumin_av
      16.09.2024 13:20
      +1

      Всегда казалось, что исключения и всякого рода коды возвратов - это вещи взаимодополняющие.

      Исключения нужны, чтобы подсветить косяк разработки. Мол, да, тут не доглядел, тут не проверил - придётся переделывать.

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


      1. MountainGoat
        16.09.2024 13:20
        +1

        В контексте сказанного вами, любая система, принуждающая обрабатывать ошибки - очень помогает "доглядеть" всё с первого раза. Естественно, есть ситуации когда надёжность - это не ценность. Одноразовую фигню, которую после первого успешного запуска выкину, я тоже на Питоне пишу и не морщусь.

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


    1. a-tk
      16.09.2024 13:20

      Спросите Java как сделать ещё хуже (спойлер: checked exceptions)


    1. bromzh
      16.09.2024 13:20
      +1

      Мсье не знает ?, expect(), anyhow

      И запретить / предупреждать про unwrap на уровне проекта бесплатно без смс:

      # Cargo.toml
      [lints.clippy]
      unwrap_used = "warn" # или "deny"


  1. aamonster
    16.09.2024 13:20
    +1

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

    2. С исключениями проблемы начинаются, когда на них начинают писать логику (не аварийная ситуация, а ситуации типа вместо проверки существования файла – обращаемся к нему и бросаем exception, если его нет). Оттенки смысла у исключений и кодов ошибок разные.


    1. MountainGoat
      16.09.2024 13:20
      +2

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

      struct Data {
        handle: int,
        file: File,
      }
      
      fn Data.magic() {
        self.handle = 256*256;
        self.file = File::open("file.txt");
        self.handle = 0;
      }

      В мире где File::open() может выбросить исключение, компилятор не имеет права удалить строку self.handle = 256*256;, потому что это повлияет на значение переменной handle в случае исключения. Поэтому огромное количество оптимизаций (даже банальные перестановки независимых строк местами) невозможно в присутствии исключений, это только самый банальный пример.


      1. aamonster
        16.09.2024 13:20
        +5

        Ну да. А в мире, где вы используете коды возврата, по результатам вызова строки 8 может случиться return, и компилятор точно так же не может выбросить строку 7.

        Но идея понятна – "явное лучше неявного" и всё такое.


        1. MountainGoat
          16.09.2024 13:20
          +1

          Это почему это? Return тут не написан. Код ошибки будет записан в file, и это уже проблема того ,кто будет file использовать в данном случае. Другое дело, что в Rust нельзя будет забыть проверить file на ошибку перед использованием.


          1. aamonster
            16.09.2024 13:20
            +2

            Тут – не написан. А как только вы начнёте переписывать File::open на использование кодов возврата вместо исключений – будет написан..


            1. MountainGoat
              16.09.2024 13:20

              По-че-му? Давайте вот вам реальный код на Расте

              use std::fs::File;
              struct Data {
                handle: i32,
                file: Option<File>,
              }
              
              impl Data {
              fn magic(&mut self) {
                self.handle = 255*255;
                self.file = File::open("example.txt").ok();
                self.handle = 0;
              }
              
              fn use_file(&mut self) {
                // Вот тут проверяем ошибку только. Причём пока не сделаем проверку
                // у нас сам объект File банально недоступен
                if let Ok(f) = self.file {
                  f.read();
                }
              }
              } // impl Data


      1. rsashka
        16.09.2024 13:20

        компилятор не имеет права удалить строку self.handle = 256*256;, потому что это повлияет на значение переменной handle в случае исключения

        А вы уверены, что это именно необходимая оптимизация компилятора, а не ошибка в программиста в логике работы программы? Просто я долго пытался придумать, для чего это могло быть нужно, и единственное назначение подобного повторного присвоения, это как раз детектирование исключения в функции File::open. Но в этом случае, подобная оптимизация будет вообще не к месту.


        1. MountainGoat
          16.09.2024 13:20
          +1

          Да, это безусловно ошибка программиста и ничто другое. Но мы все знаем, что программисты, не делающие ошибок, пишут только на С.

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

          Для машины, скомпилировать/выполнить язык и проанализировать его - это две разные задачи. Для меня первым звоночком было сравнение двух IDE от одной и той же компании, одна для Java другая для Python. Задолго до всяких ИИ. Для Java меню с готовыми рефакторингами настолько огромное, что его пришлось разбить на подменю. Вынести выделенные переменные в отдельных класс, написать геттеры, обложить тестами и т.д. и т.п. Для Python в этом же месте ровно ОДИН рефакторинг был - переименовать переменную. И тот у меня сбойнул в первую же неделю. Потому что Java можно уверенно анализировать, а Python нельзя.

          Исключения ОЧЕНЬ мешают анализировать язык и делать автоматические трансформации.


          1. rsashka
            16.09.2024 13:20

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

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


            1. MountainGoat
              16.09.2024 13:20

               И в некоторых случаях исключения реально могу значительно улучшить читаемость кода и упростить логику работы приложения (о чем и пишет автор).

              Мне вспомнилась реально виденная реклама: "У коммерческих приложений, в отличие от opensource, исходный код не доступен, поэтому вам не придётся его читать!"

              Я не назову улучшением читаемости, если мы часть поведения делаем не отражённой в коде и даже не доступной в IDE. Я не знаю ни одной IDE, где можно было бы нажать на catch, и она подсветила бы те throw, которые им покрываются, даже в других файлах и библиотеках. Я не знаю в С++ способа детерминированно убедиться, что данное исключение будет поймано, или что в данном месте кода может/не может быть непойманных исключений.


              1. rsashka
                16.09.2024 13:20
                +1

                Я честно не понял, зачем вы пишите про подсветку кода. Если у вас нет кода библиотеки, то вы точно так же не сможете перейти в IDE на все return с кодами возврата (у вас банально нет исходников). А если обработка ошибок идет с помощью исключений, то тривиальный try { func_call() } catch (...){ } ловит все исключения в func_call() детерминированно и с гарантий.


                1. MountainGoat
                  16.09.2024 13:20
                  +2

                  Объясняю.

                  то вы точно так же не сможете перейти в IDE на все return с кодами возврата

                  Допустим в Rust. Я вижу в документации или просто наведя мышкой на функцию, её заголовок. А он выглядит например так. fn download_url( url: &String) -> Result<Vec[u8], DownloadError>; И я сразу же, без дальнейших действий, знаю, что при вызове функции может случится одно из трёх:

                  • она вернёт массив с байтами, обёрнутый в Ок,

                  • она вернёт объект структуры DownloadError, обёрнутый в Err ,

                  • или случится системная паника.

                  Всё. Я точно знаю, что никаких других вариантов быть не может. Дальше, если меня колышет, почему именно скачивание не случилось, я открываю доку на DownloadError. А если нет - то и это мне не нужно, хватит знать, что он есть.

                  При исключениях же я вызываю функцию и не знаю, вернётся ли она вообще, может ли она вообще выбросить исключение, и какие именно, и сколько их - это всё никак не отображено в заголовке, и мне остаётся только надеяться, что в документации это всё перечислено. (Так вот, даже в знаменитых библиотеках часто перечислено не всё и не всегда).

                   то тривиальный try { func_call() } catch (...){ }

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

                  В Питоне даже ошибка синтаксиса - это исключение. То есть если в коде func_call() вы напишете имя переменной с опечаткой - то ваш catch это поймает, и вы даже не узнаете, что там была ошибка синтаксиса. То же, если юзер нажмёт Ctrl+C.

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


                  1. rsashka
                    16.09.2024 13:20

                    может случится одно из трёх: она вернёт массив с байтами, обёрнутый в Ок, она вернёт объект структуры DownloadError, или случится системная паника

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

                    Что касается моего кода с try { func_call() } catch (...){ }, это пример детерминированного способа ловли всех исключений с гарантий, а не коммит в opensource проект.


                    1. MountainGoat
                      16.09.2024 13:20
                      +1

                      Паника в Расте концептуально не предназначена быть обработанной. Как только кто-то вызывает панику, он знает, что убивает всё. Это не аналог исключений, это аналог if ( is_err ) sys::exit(255); Кстати, и то и то можно перехватить, но ситуации, когда это оправдано - эзотерические. Соответственно, мне как разработчику почти никогда не нужно учитывать возможность паники ранее. Не больше, чем возможность того, что весь комп повиснет. Мы же это нигде обычно не учитываем, кроме логики работы с внешним ресурсом.

                      Эмбед не в счёт, там своя атмосфера.

                      это пример детерминированного способа ловли всех исключений с гарантий,

                      А зачем нам обсуждать способы, не применимые на практике? Тут есть хаб "Ненормальное программирование", там народ на С++ такое вытворяет, что черти в аду смущаются. Но мы не будем говорить, что в С++ есть такие возможности.


                      1. rsashka
                        16.09.2024 13:20

                        Паника в Расте концептуально не предназначена быть обработанной.

                        Так про это я писал в самом первом комментарии к статье, насчет обоснования почему обработку ошибок нужно делать именно так - потому что по другому не получается.

                        А зачем нам обсуждать способы, не применимые на практике?

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

                        Это ваши слова и в них речь шла именно про С++, а не про Python или абстрактную OpenSource библиотеку.


                      1. MountainGoat
                        16.09.2024 13:20

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

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

                        Код ошибки же придётся обработать (или явно проигнорировать) в каждом месте, где используется обёрнутое им значение, поэтому обозначенный вопрос даже не стоит.


                      1. rsashka
                        16.09.2024 13:20

                        Автор статьи пишет про другое, что в некоторых случаях исключения использовать удобнее (и это действительно так). Так почему вы ему отказываете в этой возможности? Только на том основании, что в Rust идеологически не реализованы исключения? Ведь автор в приведенных в статье примерах реально показал, что использовать исключения с умом (там где они есть) бывает полезно и удобно.

                        Код ошибки же придётся обработать (или явно проигнорировать) в каждом месте, где используется обёрнутое им значение, поэтому обозначенный вопрос даже не стоит.

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


                      1. MountainGoat
                        16.09.2024 13:20

                        По форумам стоит вой на тему "Если я хочу использовать библиотеку, а она async, то мне тоже по всей программе приходится вводить async." Видимо, народ это серьёзно напрягает. Меня - нет, поэтому комментировать это я не могу.

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

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

                        Наконец автор просто врёт. Его пример с рекурсивной фибоначей просто написан неправильно. Там нужно было использовать and_then. Ну и сразу возникают вопросы про компилятор и режимы оптимизации.

                        За такое в Древней Греции били керопеджией по просопу.


                1. DancingOnWater
                  16.09.2024 13:20

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


    1. fshp
      16.09.2024 13:20
      +1

      ситуации типа вместо проверки существования файла – обращаемся к нему и бросаем exception, если его нет

      Проверили, что файл есть. После этого другой процесс его удалил. И мы с полной уверенностью идём открывать этот файл.


      1. aamonster
        16.09.2024 13:20
        +2

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

        Банально: если у нас мало эксепшнов и они случаются только в важных ситуациях – код проще отлаживать. Вплоть до того, что можно ставить breakpoint on thrown exception.


  1. vadimr
    16.09.2024 13:20
    +3

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

    Это не так. Код с обработкой исключений сложно распараллелить и тем более конвейеризовать.


    1. rsashka
      16.09.2024 13:20

      Код с обработкой исключений сложно распараллелить ...

      Не расскажите, почему? Насколько мне известно, обработка исключений в отдельном потоке ничем не отличается от обработки исключений в основном потоке приложения. И если логика параллелиться, то она будет параллелиться не зависимо от наличия или отсутствия исключений (тем более, что генерация исключений в STL или рантайме никуда не девается).


      1. vadimr
        16.09.2024 13:20
        +1

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

        А векторных операциях даже сам процессор не гарантирует синхронности исключений с вычислениями.


        1. rsashka
          16.09.2024 13:20

          Другими словами, для ручной обработки потоков, нет разницы, используются исключения или нет, а в отдельных случаях реализации распараллеливания (каких нибудь async и await), использование исключений будет определяется реализацией.

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


          1. vadimr
            16.09.2024 13:20

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

            Если вы, например, словили своё исключение по делению на ноль посреди цикла (скажем, for (...) { a[i] = 1.0 / b[i];}), то обработчик исключений (который, возможно, находится в совсем другом модуле) обязан исходить из предположения, что цикл уже выполнился для начальных ненулевых элементов. А это очень дорогое предположение при определённых обстоятельствах. В то время как проверка через if может быть асинхронна.


            1. rsashka
              16.09.2024 13:20
              +1

              обработка исключений подразумевает синхронное выполнение ветки исключения по отношению к основной ветке кода

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

              Про цикл ничего не понял. Ну словили вы деление на ноль, но откуда берутся обязательства в обработчике, причем тут вообще массив? Вы же сами закладываете логику обработчика, но на нее нет никаких подразумеваемых обязательств (кроме требований стандарта).

              В то время как проверка через if может быть асинхронна.

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


              1. vadimr
                16.09.2024 13:20

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

                Про цикл ничего не понял. Ну словили вы деление на ноль, но откуда берутся обязательства в обработчике, причем тут вообще массив?

                Притом, что массив – это главное основание для параллельных вычислений.

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

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

                Можете привести пример?

                for (i=0; i<N; i++) {

                a[i] = 1.0 / (abs (b[i]) < eps ? 1.0 : b[i]);

                } // такой цикл может быть эффективно распараллелен и векторизован и выполняться в произвольном порядке, на произвольном количестве ядер и арифметико-логических устройств.

                try {

                for (i=0; i<N; i++) {

                a[i] = 1.0 / b[i]);

                }

                catch {

                ... ошибка в i-м элементе! ...

                } // такой цикл должен выполняться строго последовательно от 0 до N-1 в одном потоке


                1. rsashka
                  16.09.2024 13:20

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

                  Значит я не правильно понял ваш предыдущей ответ:

                  подразумевает синхронное выполнение ветки исключения по отношению к основной ветке кода

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


                  1. vadimr
                    16.09.2024 13:20

                    Разумеется, они не эквивалентны! Я об этом и пишу.

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

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


                    1. rsashka
                      16.09.2024 13:20

                      В первом коде вы не возвращаете индекс элемента при ошибке, так откуда он появляется во втором примере?

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


                      1. vadimr
                        16.09.2024 13:20

                        Так как раз именно потому, что нет возврата, всё и работает эффективно.

                        А обработка ошибок есть, она в условном выражении.

                        Во втором примере нам индекс элемента нужен, чтобы занулить ошибочное значение в обработчике.

                        Что касается nan, то давайте не будем менять условия задачи на ходу. В статье написано про деление на ноль – я показал, как это пишется, если нужна эффективность. Про nan тоже можно расписать, но это ничего по существу не добавит.


                      1. rsashka
                        16.09.2024 13:20

                        Так как раз именно потому, что нет возврата, всё и работает эффективно.

                        Вы сравниваете два не эквивалентных фрагмента кода. Бессмысленно сравнивать скорости работы двух программ, если они делают разные расчеты.


                      1. vadimr
                        16.09.2024 13:20

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


                      1. rsashka
                        16.09.2024 13:20

                        Давайте попробует вернуться к вашему первому утверждению

                        Если вы, например, словили своё исключение по делению на ноль посреди цикла (скажем, for (...) { a[i] = 1.0 / b[i];}), то обработчик исключений (который, возможно, находится в совсем другом модуле) обязан исходить из предположения, что цикл уже выполнился для начальных ненулевых элементов. А это очень дорогое предположение при определённых обстоятельствах. В то время как проверка через if может быть асинхронна.

                        Я вообще не понял, что вы хотите показать (доказать) с помощью приведенных примеров. Ведь изначально речь шла про возможность оптимизации, верно?


                      1. vadimr
                        16.09.2024 13:20

                        Да всё верно. Обработчик ошибок с помощью условного оператора может быть автоматически распараллелен и векторизован (например, этот цикл может быть автоматически выгружен в GPU или распараллелен на 80 ядер CPU), так как это просто N независимых вычислительных процессов. А с помощью исключений – не может, так как распараллеливание теряет контекст, а обработчик контекста один.


                      1. rsashka
                        16.09.2024 13:20

                        Отлично, теперь идем дальше.

                        Где в вашем первом примере условный оператор, который возвращает код ошибки и который может быть "автоматически распараллелен и векторизован"? Его нет (есть тернарная условная операция, которая нормально параллелится не зависимо от обработки исключений).


                      1. vadimr
                        16.09.2024 13:20

                        А кто вам обещал возвращать код ошибки?

                        Вы постоянно почему-то хотите, чтобы я вам на условных операторах сэмулировал поведение обработчика исключений. А я не ставлю перед собой такой цели.

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

                        Мне его обработчик деления на ноль нафиг не сдался, если он простым своим присутствием замедляет программу во много раз.


                      1. rsashka
                        16.09.2024 13:20

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

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

                          if( check ){
                            throw  "Error";
                          }
                          if( check ){
                            return ERROR_CODE;
                          }
                        

                        Я же от вас прошу примера, в чем тут по вашему мнению состоит неправда.

                        Мне его обработчик деления на ноль нафиг не сдался, если он простым своим присутствием замедляет программу во много раз.

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


                      1. vadimr
                        16.09.2024 13:20

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


                      1. rsashka
                        16.09.2024 13:20

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

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


                      1. vadimr
                        16.09.2024 13:20

                        Вы понимаете, что для опровержения общего утверждения достаточно одного примера?

                        Что касается вашего кода, то в нём нет ничего, эффективность чего можно было бы улучшить или ухудшить.


  1. NeoNN
    16.09.2024 13:20
    +1

    Исключения допустимы тогда, когда они не превращаются во второй контур логики и перехватываются как можно ближе к точке возникновения для ретраев или прокидывания в ошибку. В остальной логике гораздо удобнее и понятнее следовать парадигме railway oriented с Result<T> и обработкой Success и Fail. Потребовались годы, на самом деле, чтобы это осознать, набив шишек.


  1. domix32
    16.09.2024 13:20

     в рассмотренном ниже примере реализация на C++ с использованием исключений примерно в четыре раза быстрее, чем на Rust.

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

    Почему он перезаворачивает unexpected тоже вопрос отдельный


  1. ImagineTables
    16.09.2024 13:20
    +1

    Исключения (и, кстати говоря, промисы тоже) плохи тем, что они подменяют пусть и сложный, но правильный поток (flow) упрощённой версией. Иногда переупрощённой. Та простота, что хуже воровства. Пример можно взять прямо отсюда, с Хабра: недавно была статья, в которой автор жаловался, что в Go он не может сделать примерно так (пишу по памяти, но вряд ли сильно ошибаюсь):

    try
    {
        данные = запросить_данные_из_БД();
        новые_данные = обработать_данные(данные);
        положить_данные_в_БД(новые_данные);
    }
    catch (...) // Паттерн "покемон": "Поймай их всех!"
    {
    …
    }
    

    Представим себе, что запросить_данные_из_БД() — сама по себе дорогая операция, а обработать_данные() берёт свободный ресурс из пула (специализированное ядро, например), вешает на него таску и счастливо джойнится до конца исполнения. И внезапно менеджер пула подвисает и перезапускается.

    Что можно было бы сделать?

    Как минимум, дать ему несколько шансов, если запрос менеджера пулов обломился. То есть, do {res = обработать_данные(данные); } while (pm_timed_out == res && i++ < 10);. Именно в эту сторону, как я понял, нас и толкает Go. (Я на нём не писал, а про его обработку ошибок узнал только из критической статьи). Автору это не понравилось, и он придумал какой-то способ это обойти и приблизиться к своему идеалу (см. выше).

    И теперь данные из запросить_данные_из_БД(); пропадут. Ведь в лучшем случае из catch-секции мы придём туда, откуда заново запустим ту же функцию.

    Зато всё просто и наглядно. Даже слишком.

    Как по мне, исключения должны быть действительно исключительными ситуациями, а всё остальное в нашем потоке должно быть поточным, т.е. без прыжков туда-сюда. В полном соответствии с названиями.

    И то же самое относится к промисам. Не надо с их помощью спрямлять flow сильнее, чем позволяет здравый смысл.


  1. IsKaropk
    16.09.2024 13:20

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

    Я около 10 лет назад "на коленке" сделал серверное приложение, которое именно вылетало в некоторых (редких) критических ситуациях. Ну вот нужно было "к утру" (не шутка), разбираться некогда было. Сервис Виндоус, после падения автоматом перезапускается.

    Почти 10 лет прошло. Я ещё помню об этом техдолге, и я к нему обязательно вернусь. Как только все остальные дела переделаю...