Как стать автором
Обновить

Комментарии 23

Кирилл, статья интересная спасибо. Но с запятыми у вас всё плохо, читать тяжело.

Спасибо за положительный отзыв о содержании статьи. Что касается формы... Приложу дополнительные усилия для проверки.

Плюс вам за рекомендацию anyhow, на мой взгляд отлично сбалансированная библиотека для небольших проектов.

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

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

Дык, в описании библиотеки прямо говорится:


Use Anyhow if you don't care what error type your functions return, you just want it to be easy. This is common in application code. Use thiserror if you are a library that wants to design your own dedicated error type(s) so that on failures the caller gets exactly the information that you choose.

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

Знаете, чем мне не нравится передача ошибок (как в Go и Rust) и нравятся исключения?
Тем, что при передаче ошибок приложение становится гораздо сложнее отлаживать. Теряется информация, откуда именно пришла ошибка. При использовании исключений можно вывести stack trace до места, где возникло это самое исключение.
Тем, что невозможно заранее узнать, какие ошибки может возвращать функция. В принципе, с исключениями тоже не всегда можно узнать, какая функция какое исключение может кинуть, но обычно для этого есть если не языковые конструкции (как throws в Java), то хотя бы комментарии (как в PHP).
Использование panic в чем-то похоже на исключение, но не позволяет указать его тип — просто некая глобальная ошибка, которую можно где-то отловить, и все, что про нее известно, это ее текстовое описание.

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

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

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

Ну так и исключения почти бесплатные только пока их нет

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

При использовании исключений можно вывести stack trace до места, где возникло это самое исключение.

Всё-таки раст язык более низкоуровневый и с джавой или C# сравнивать не совсем честно. В С++(23) стектрейсы только хотят завезти.


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


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

Разве в джаве это не признали неудачным решением? Мне казалось, что все пользуются RuntimeException. Нет?


А насчёт PHP можно подробнее, а то я, конечно, полез гуглить, но не понял проверяется ли как-то соответствие комментариев действительности. Что будет, если документация врёт? А если перечислены не все исключения? В общем, если всё держится на внимательности разработчика, то такое себе.


Использование panic в чем-то похоже на исключение, но не позволяет указать его тип

Справедливости ради, позволяет. Можно использовать panic_any, catch_unwind, Any::downcast_ref и resume_unwind чтобы эмулировать исключения. Другое дело, что это будет громоздко, неудобно, да и в целом не рекомендуется.

Тем, что невозможно заранее узнать, какие ошибки может возвращать функция.

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

При использовании того же anyhow и with_context вполне можно получить «человеческий» стектрейс. Отсутствие спецификации исключений на мой взгляд это серьёзный недостатков, и ни checked exceptions, ни комментарии (допустим у вас нет исходников, да и смотреть каждый раз на комментарии, которые ещё и измениться могут, а вы этого не заметите) тут не особо помогают.

В go вы можете обернуть ошибку, и добавить дополнительную инфу к ошибке и получить тот же стектрейс

(Это с go 1.13 подробнее )

С другой стороны в C++ исключения создают ощущение что каждая функция это обработчик прерываний (конечно если нужно написать надежный код, так конечно можно забить). Для каждой строчки нужно умать, а что если здесь вылетит исключение. При этом ведь даже неизвестно будет или нет исключение, если не используется noexcept, документация может врать и нужно читать исходники. И всякие конструкторы, operator= тоже могут и бывают написаны без раздумий над тем, а что будет если внутри них произойдет исключение, что превращает даже банальный swap в непредсказумую фигню в которой могут "утечь" ресурсы.

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

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

в go (яп с gc) есть функция отложенного вызова defer - она выполнится перед выходом из функции. Стандартный способ работы (например с бд, но также можно и со всем остальным - файлы, сокет, тп), выглядит так:

err := db.Open(ctx) 
if err != nil {
  return err
}
defer db.Close()
// some code with db

При этом сначала выполнится код после defer , потом (перед выходом из функции) LIFO выполняются defer, и не надо контролировать закрытие больше

Минус такого подхода в том, что надо каждый раз не забыть написать соответствующий defer. А без GC (с RAII) можно просто предоставить объект "подключение к базе" (или сокет, файл и т.д.), который в деструкторе будет делать то, что нужно. В итоге пользовательский код становится проще и забыть "закрыть ресурс" нельзя.

Отличная статья, спасибо. После rust-book осталось ощущение что тема ошибок не до конца раскрыта. thiserror - хорош. Странно, что его нет в стандартной библиотеке, писать boilrerplate для преобразования ошибок очень утомительно, и не сильно читабельно.

К такой обработке ошибок не с первой итерации пришли. Если бы торопились в стандартную библиотеку добавлять, то сейчас там был бы кривой error_chain.

Интересто, а ведь std::error::Error для этих же целей задумывался, но как-то не прижился...

Почему не прижился? Это базовый типаж, им все пользуются - в том числе и вышеупомянутый thiserror.

А про текущий статус поисков идеально ржавого способа работать с ошибками можно почитать https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html

Зарегистрируйтесь на Хабре, чтобы оставить комментарий