Перейти к основному содержимому
  1. Rust/

Обработка ошибок в Rust: паника, Option, Result и как с ними дружить

1360 слов·7 минут· loading · loading · · ·Rust-middle
Оглавление
about-rust - Эта статья часть цикла.
Статей прочитано 0/36
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
Не прочитана
Не прочитана
Не прочитана
Не прочитана
🔵 Средний уровень (Rust-middle)

Введение
#

Ошибки в коде случаются у всех. Вопрос лишь в том, как язык программирования помогает нам с ними справляться. За годы развития 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; // Все ок
}
  • В чем проблема:
    1. Разработчик может легко проигнорировать возвращаемый код и работать дальше с пустыми данными.
    2. Успешный результат и ошибка смешиваются в рамках одного и того же типа int.

C++, Java, C#, Python, JS: Исключения (Exceptions)
#

В этих языках для сообщения об ошибке используют конструкцию throw / raise, которая прерывает выполнение кода и летит вверх, пока кто-то не поймает её в блок catch (или except).

void process() {
    try {
        do_work();
    } catch (const std::exception& e) {
        // Ловим ошибку здесь
    }
}
  • В чем проблема:
    1. Ошибку все еще легко случайно проигнорировать, написав пустой блок catch.
    2. Исключения сильно замедляют программу в момент их выброса (оверхед на раскрутку стека).
    3. Сигнатура функции не говорит о том, какие именно исключения она может выбросить. Код становится непредсказуемым.

Язык Go: Возврат пары значений
#

В Go функции возвращают кортеж (результат, ошибка).

res, err := doSomething()
if err != nil {
    return nil, err
}
  • В чем проблема:
    1. Ошибку легко заглушить, присвоив ее символу _.
    2. Приходится писать огромное количество шаблонного кода 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
#

Любой наш тип ошибки должен реализовывать трейты форматирования:

  1. Debug — технический вывод для логов (выводится через {:?}, легко сгенерировать с помощью #[derive(Debug)]).
  2. Display — красивый текст для людей (выводится через {}).

Библиотеки-помощники
#

Чтобы не писать тонны рутинного кода для вывода ошибок, разработчики используют готовые крейты:

  • anyhow (для приложений): Отлично подходит для конечных программ (application-level). Позволяет свести все возможные ошибки в проекте к одному типу anyhow::Error и не расписывать их вручную.
  • thiserror (для библиотек): Идеально подходит для библиотек и переиспользуемых модулей. Автоматически создает красивый вывод ошибок (Display) и приведения типов на основе простых аннотаций прямо над перечислением.
  • snafu: Еще один мощный инструмент для гибкой настройки ошибок с сохранением контекста.

Давайте посмотрим, как работать с Option, Result, оператором ? и библиотекой thiserror на интерактивных примерах кода:

Обработка ошибок в Rust
Шаг 1/3
 1fn main() {
 2    let raw_input: Option<&str> = Some("42");
 3    
 4    // Преобразуем строку в число и умножаем на 2
 5    let processed = raw_input
 6        .and_then(|s| s.parse::<i32>().ok())
 7        .map(|num| num * 2);
 8        
 9    println!("Обработанное значение: {:?}", processed);
10}
11

Комбинаторы Option и Result

  • Вместо матчинга можно лаконично преобразовывать данные с помощью map и and_then.
  • Метод map применяет функцию к значению внутри Some или Ok.
  • Метод and_then принимает замыкание, возвращающее новый Option/Result — удобно для цепочки зависимых операций.

Проверь свои знания!
#

Пройдите короткий интерактивный тест, чтобы закрепить понимание обработки ошибок в Rust.

История обновлений

  • Создана новая статья о глубокой обработке ошибок в Rust, монадических типах, операторе ? и кастомных ошибках с примерами кода и интерактивными тестами.
Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
about-rust - Эта статья часть цикла.
Статей прочитано 0/36
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
Не прочитана
Не прочитана
Не прочитана
Не прочитана
🔵 Средний уровень (Rust-middle)

Связанные статьи