Введение#
Ошибки в коде случаются у всех. Вопрос лишь в том, как язык программирования помогает нам с ними справляться. За годы развития IT-индустрии разработчики перепробовали много разных подходов: от простых возвращаемых циферок-кодов до сложных и тяжелых исключений.
Rust пошел своим путем. Вместо скрытых падений и неявного поведения он предлагает нам два четких инструмента: невосстановимые ошибки (когда продолжать работу бессмысленно) и восстановимые ошибки через специальные типы-контейнеры Option<T> и Result<T, E>.
Давайте разберем, как работают ошибки в разных языках, научимся пользоваться методами стандартной библиотеки Rust и разберемся, зачем нужны сторонние библиотеки вроде thiserror и anyhow.
1. Как с ошибками борются в других языках?#
Язык C: Коды ошибок#
В C функции обычно возвращают числовой код ошибки (например, -1 при сбое и 0 при успехе), а полезные данные передаются через указатели в аргументах.
int get_data(input in, output *out) {
if (something_wrong) {
return -1; // Ошибка
}
return 0; // Все ок
}- В чем проблема:
- Разработчик может легко проигнорировать возвращаемый код и работать дальше с пустыми данными.
- Успешный результат и ошибка смешиваются в рамках одного и того же типа
int.
C++, Java, C#, Python, JS: Исключения (Exceptions)#
В этих языках для сообщения об ошибке используют конструкцию throw / raise, которая прерывает выполнение кода и летит вверх, пока кто-то не поймает её в блок catch (или except).
void process() {
try {
do_work();
} catch (const std::exception& e) {
// Ловим ошибку здесь
}
}- В чем проблема:
- Ошибку все еще легко случайно проигнорировать, написав пустой блок
catch. - Исключения сильно замедляют программу в момент их выброса (оверхед на раскрутку стека).
- Сигнатура функции не говорит о том, какие именно исключения она может выбросить. Код становится непредсказуемым.
- Ошибку все еще легко случайно проигнорировать, написав пустой блок
Язык Go: Возврат пары значений#
В Go функции возвращают кортеж (результат, ошибка).
res, err := doSomething()
if err != nil {
return nil, err
}- В чем проблема:
- Ошибку легко заглушить, присвоив ее символу
_. - Приходится писать огромное количество шаблонного кода
if err != nilна каждом шагу.
- Ошибку легко заглушить, присвоив ее символу
Haskell, Scala, TypeScript#
Используют так называемые алгебраические типы данных (ADT) и обертки для значений (Option/Either). Именно эта идея легла в основу Rust, позволив сделать обработку ошибок безопасной и красивой.
2. Невосстановимые ошибки: Паника (panic!)#
Иногда в программе происходит то, к чему мы вообще не готовы: например, выход за границы массива или деление на ноль. В Rust такие ситуации приводят к панике.
- Что такое паника: Это мгновенная остановка выполнения текущего потока.
- Главное правило: Программа не должна пытаться обрабатывать панику в обычном коде. Лучшее решение при панике — упасть, чтобы не испортить данные в базе данных или файлы на диске.
- Как вызвать: С помощью макроса
panic!("Сообщение об ошибке")или методов извлечения значения.unwrap()/.expect(), если они вызваны на пустом значении.
3. Восстановимые ошибки: Обертки Option и Result#
Если ошибка ожидаема (например, файл не найден или пользователь ввел буквы вместо числа), мы используем типы-контейнеры.
Тип Option<T>#
Используется, когда значение может просто отсутствовать.
pub enum Option<T> {
None, // Значения нет
Some(T), // Значение есть и внутри лежит тип T
}Благодаря этому, компилятор заставит вас обработать случай None перед тем, как вы сможете получить доступ к данным внутри Some.
Тип Result<T, E>#
Используется, когда операция может завершиться ошибкой, и нам важно знать её причину.
pub enum Result<T, E> {
Ok(T), // Успех, внутри лежит результат типа T
Err(E), // Сбой, внутри лежит ошибка типа E
}Тип Result помечен специальным атрибутом #[must_use]. Если функция возвращает Result, а вы проигнорируете его (не сохраните в переменную и не обработаете), компилятор выдаст предупреждение.
4. Шпаргалка по методам Option и Result#
Для работы со значениями внутри Option и Result стандартная библиотека предоставляет множество удобных методов.
| Группа методов | Для Option<T> | Для Result<T, E> | Что делает / Нюансы |
|---|---|---|---|
| Жёсткое извлечение (Паника при сбое) | unwrap()expect(&str) | unwrap()expect(&str)unwrap_err() | Достают значение. Если внутри None/Err — программа падает. В expect можно передать сообщение для лога. unwrap_err достает ошибку, паникуя при успехе. |
| Безопасное извлечение по умолчанию | unwrap_or(default)unwrap_or_else(f)unwrap_or_default() | unwrap_or(default)unwrap_or_else(f)unwrap_or_default() | Возвращают значение или дефолтный вариант. В _or_else дефолт вычисляется лениво через функцию f. |
| Простые проверки | is_some()is_none() | is_ok()is_err() | Возвращают true или false. Не забирают владение значением. |
| Трансформация содержимого | map(f) | map(f)map_err(f) | Применяют функцию f к значению внутри, если оно есть. Противоположное состояние не трогают. map_err меняет только тип ошибки. |
| Логические цепочки (И) | and(opt)and_then(f) | and(res)and_then(f) | and_then (часто называют flat_map) принимает функцию, которая возвращает новый Option или Result. Позволяет выстраивать цепочки без вложенности. |
| Логические цепочки (ИЛИ) | or(opt)or_else(f) | or(res)or_else(f) | Возвращают оригинал, если он успешный. Если там None/Err — возвращают альтернативу. |
| Преобразования типов | transpose()ok_or(err)ok_or_else(f) | transpose()ok()err() | transpose меняет вложенность: Option<Result> превращает в Result<Option>. ok_or превращает Option в Result, заменяя None на указанную ошибку. ok() и err() делают из Result опциональные значения. |
5. Лаконичный проброс ошибок: Оператор ?#
Чтобы не писать постоянно конструкции match или if let, в Rust есть оператор ?. Он ставится в конце выражения, возвращающего Option или Result.
let parsed = val_str.parse::<i32>()?;Как он работает:
- Если внутри
ResultлежитOk, оператор?просто достает чистое значение и код идет дальше. - Если внутри лежит
Err, выполнение функции немедленно прекращается, и эта ошибка возвращается наверх в вызывающий код. - Магия приведения типов: Под капотом компилятор автоматически вызывает метод
From::from(error). Это значит, что если ваша функция возвращает большую общую ошибку, а локальная функция вернула маленькую специфичную ошибку, Rust сам превратит её в нужный тип, если для них реализована конвертация.
6. Создание своих типов ошибок#
В реальных проектах нам часто нужно детально описывать свои ошибки.
Стандартный трейт std::error::Error#
Любой наш тип ошибки должен реализовывать трейты форматирования:
Debug— технический вывод для логов (выводится через{:?}, легко сгенерировать с помощью#[derive(Debug)]).Display— красивый текст для людей (выводится через{}).
Библиотеки-помощники#
Чтобы не писать тонны рутинного кода для вывода ошибок, разработчики используют готовые крейты:
anyhow(для приложений): Отлично подходит для конечных программ (application-level). Позволяет свести все возможные ошибки в проекте к одному типуanyhow::Errorи не расписывать их вручную.thiserror(для библиотек): Идеально подходит для библиотек и переиспользуемых модулей. Автоматически создает красивый вывод ошибок (Display) и приведения типов на основе простых аннотаций прямо над перечислением.snafu: Еще один мощный инструмент для гибкой настройки ошибок с сохранением контекста.
Давайте посмотрим, как работать с Option, Result, оператором ? и библиотекой thiserror на интерактивных примерах кода:
Проверь свои знания!#
Пройдите короткий интерактивный тест, чтобы закрепить понимание обработки ошибок в Rust.
История обновлений
- Создана новая статья о глубокой обработке ошибок в Rust, монадических типах, операторе
?и кастомных ошибках с примерами кода и интерактивными тестами.

