Обработка ошибок в Rust: try/catch и не только — продвинутый уровень

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

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

Начать курс

Обработка ошибок в Rust: try/catch и не только — продвинутый уровень

В отличие от многих языков, где исключения — это стандартный механизм (как try/catch в Python, Java или C++), Rust предлагает принципиально иной подход. Здесь нет исключений в классическом понимании. Вместо этого язык использует мощную систему типов для представления и обработки ошибок. Для новичка это может показаться сложным, но на продвинутом уровне вы оцените безопасность и предсказуемость, которые даёт такой подход. В этой статье мы разберём, как работать с ошибками в Rust профессионально: от базовых Result и Option до продвинутых крейтов anyhow и thiserror, а также проведём параллели с привычным try/catch.

1. Фундамент: Result, Option и оператор ?

Основа обработки ошибок в Rust — это перечисление Result<T, E>. Оно может быть либо Ok(T) (успех), либо Err(E) (ошибка). Аналогом try/catch можно считать оператор ?. Он упрощает проброс ошибки наверх: если значение — Err, функция немедленно возвращает его; если Ok — извлекает значение.

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 content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

В этом примере File::open и read_to_string могут вернуть Err. Оператор ? заменяет громоздкий match и делает код чистым, как try/catch в других языках. Однако, в отличие от исключений, вы явно указываете тип ошибки в сигнатуре функции — это повышает читаемость и безопасность.

Перечисление Option<T> используется для случаев, когда значение может отсутствовать (аналог None в Python). Оператор ? работает и с ним:

fn get_first_element(vec: &Vec<i32>) -> Option<&i32> {
    vec.first()? // Если вектор пуст, возвращаем None
}

2. Продвинутая работа с типами ошибок: трейты From и Error

Когда функция вызывает несколько операций с разными типами ошибок (например, ввод-вывод и парсинг), возникает проблема: как вернуть единый тип? Решение — трейт From и пользовательский тип ошибки.

Создадим свою структуру ошибки и реализуем для неё трейт std::error::Error (автоматически через #[derive(Debug)] и ручную реализацию Display):

use std::fmt;
use std::io;

#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(std::num::ParseIntError),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {}", e),
            MyError::Parse(e) => write!(f, "Parse error: {}", e),
        }
    }
}

impl std::error::Error for MyError {}

// Реализуем From для автоматического преобразования
impl From<io::Error> for MyError {
    fn from(error: io::Error) -> Self {
        MyError::Io(error)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(error: std::num::ParseIntError) -> Self {
        MyError::Parse(error)
    }
}

fn process_data(path: &str) -> Result<i32, MyError> {
    let content = std::fs::read_to_string(path)?; // Автоматически преобразуется в MyError::Io
    let number: i32 = content.trim().parse()?;    // Автоматически преобразуется в MyError::Parse
    Ok(number)
}

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

3. Крейты для упрощения: thiserror и anyhow

Ручное написание кода для From и Display может быть утомительным. Здесь на помощь приходят популярные крейты.

thiserror — для библиотек

Крейт thiserror позволяет генерировать код ошибок с помощью derive-макроса. Идеально, если вы пишете библиотеку и хотите предоставить пользователю конкретные типы ошибок.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyLibError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
    #[error("Custom error: {msg}")]
    Custom { msg: String },
}

fn lib_function() -> Result<(), MyLibError> {
    // ...
    Ok(())
}

Макрос #[from] автоматически реализует From, а #[error("...")] — Display. Это сокращает boilerplate-код на 90%.

anyhow — для приложений

Крейт anyhow предоставляет универсальный тип ошибки anyhow::Error, который может хранить любую ошибку, реализующую std::error::Error. Он идеален для приложений, где не важны конкретные типы ошибок, а нужно просто обработать или показать пользователю сообщение.

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let content = std::fs::read_to_string("config.toml")
        .with_context(|| "Не удалось прочитать конфигурационный файл")?;
    let value: i32 = content.trim().parse()
        .with_context(|| "Не удалось распарсить число из конфига")?;
    println!("Значение: {}", value);
    Ok(())
}

Метод .with_context() добавляет понятное описание к ошибке, что очень полезно для логирования. Вместо сухого "No such file or directory" вы получите

Блоги

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