Обработка ошибок в Rust: try/catch и не только

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение: философия обработки ошибок в Rust

В отличие от многих языков с исключениями (Java, Python, C++), Rust не использует классическую конструкцию try/catch. Вместо этого язык предлагает более явный и типобезопасный подход. Ошибки в Rust — это не скрытые потоки управления, а возвращаемые значения. Это делает код предсказуемым и защищает от «тихих» сбоев.

В Rust есть два основных типа для представления ошибочных ситуаций:

  • Result<T, E> — для восстанавливаемых ошибок (ошибки ввода-вывода, парсинга, бизнес-логики).
  • Option<T> — для отсутствия значения (аналог null, но безопасный).

В этой статье мы разберём, как работать с этими типами, чем их заменить из мира try/catch, и как писать надёжный код на Rust.

1. Result<T, E>: главный инструмент обработки ошибок

Тип Result — это enum с двумя вариантами: Ok(T) (успех) и Err(E) (ошибка). Он используется в большинстве функций, которые могут завершиться неудачей.

1.1. Создание и возврат Result

use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;  // оператор ? — аналог try!
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

Здесь оператор ? — это ключевой элемент. Он работает как try в других языках: если результат Err, функция немедленно возвращает эту ошибку. Если Ok — извлекает значение.

1.2. Обработка с помощью match

Самый явный способ — использовать match:

let result = read_file("config.txt");

match result {
    Ok(content) => println!("Файл прочитан: {}", content),
    Err(e) => eprintln!("Ошибка: {}", e),
}

Это аналог try/catch, но более гибкий и контролируемый.

1.3. Вспомогательные методы

  • .unwrap() — паникует при ошибке (только для прототипов).
  • .expect(msg) — паникует с кастомным сообщением.
  • .unwrap_or(default) — возвращает значение по умолчанию.
  • .is_ok() / .is_err() — проверка состояния.

2. Option<T>: безопасная работа с отсутствием значения

Option — это ещё один enum с вариантами Some(T) и None. Он заменяет null и nullptr, исключая ошибки разыменования нулевого указателя.

2.1. Пример использования

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

let user = find_user(2);
println!("Пользователь: {}", user.unwrap_or("Гость".to_string()));

2.2. Комбинация с Result

Часто Option преобразуется в Result для единообразной обработки:

fn get_user_name(id: u32) -> Result<String, String> {
    find_user(id).ok_or("Пользователь не найден".to_string())
}

Метод .ok_or() превращает Some в Ok, а None — в Err.

3. Оператор ?: элегантная замена try/catch

Оператор ? — это синтаксический сахар, который автоматически распаковывает Result или Option. Он доступен только в функциях, возвращающих соответствующий тип.

3.1. Пример с цепочкой вызовов

use std::num::ParseIntError;

fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x: i32 = a.parse()?;
    let y: i32 = b.parse()?;
    Ok(x + y)
}

Без ? пришлось бы писать громоздкий match для каждого вызова. Оператор ? делает код таким же чистым, как try/catch, но с явными типами.

3.2. Работа с Option

Оператор ? работает и с Option:

fn get_first_char(s: Option<&str>) -> Option<char> {
    let s = s?;  // если None — выходим
    s.chars().next()
}

4. Паника: когда ошибка фатальна

В Rust есть механизм паники (panic!) — это аварийное завершение с размоткой стека. Он используется для невосстанавливаемых ошибок (например, выход за границы массива).

Лучшая практика: паника — для багов и инвариантов, Result — для внешних ошибок.

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Деление на ноль!");
    }
    a / b
}

В production-коде такие функции стоит заменять на Result.

5. Пользовательские типы ошибок

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

use std::fmt;

#[derive(Debug)]
enum MyError {
    IoError(std::io::Error),
    ParseError(String),
    NotFound,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::IoError(e) => write!(f, "Ошибка ввода-вывода: {}", e),
            MyError::ParseError(s) => write!(f, "Ошиб                

Блоги

Рекомендаций по книгам