Обработка ошибок в Rust: от unwrap до продвинутых техник

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

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

Начать курс

Обработка ошибок в Rust: от unwrap до продвинутых техник

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

1. Почему Rust отказался от try/catch?

В языках с исключениями поток выполнения может быть прерван в любой момент. Это усложняет анализ кода и может привести к утечкам ресурсов. Rust предлагает два основных подхода:

  • Восстанавливаемые ошибки — тип Result<T, E> (успех Ok(T) или ошибка Err(E)).
  • Неисправимые ошибки — макрос panic!, который останавливает программу.

Оператор ? — это Rust-эквивалент try. Он извлекает значение из Ok или немедленно возвращает Err из функции.

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

fn read_username(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?; // если ошибка — возвращаем её
    let mut username = String::new();
    file.read_to_string(&mut username)?; // снова "try"
    Ok(username.trim().to_string())
}

2. Создание собственных типов ошибок

Для сложных проектов одного std::io::Error недостаточно. Лучшая практика — определить свой тип ошибки с помощью перечисления (enum). Это позволяет группировать разные виды ошибок и добавлять контекст.

use std::fmt;

#[derive(Debug)]
enum MyAppError {
    IoError(std::io::Error),
    ParseError(String),
    NetworkError { url: String, status: u16 },
}

impl fmt::Display for MyAppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyAppError::IoError(e) => write!(f, "IO error: {}", e),
            MyAppError::ParseError(s) => write!(f, "Parse error: {}", s),
            MyAppError::NetworkError { url, status } => {
                write!(f, "Network error for {}: status {}", url, status)
            }
        }
    }
}

impl std::error::Error for MyAppError {} // реализуем трейт Error

Теперь можно использовать ? для автоматической конвертации, если реализовать трейт From:

impl From<std::io::Error> for MyAppError {
    fn from(err: std::io::Error) -> MyAppError {
        MyAppError::IoError(err)
    }
}

fn process_file(path: &str) -> Result<(), MyAppError> {
    let content = std::fs::read_to_string(path)?; // автоматически конвертируется
    if content.is_empty() {
        return Err(MyAppError::ParseError("empty file".into()));
    }
    Ok(())
}

3. Продвинутые крейты: thiserror и anyhow

Писать вручную все реализации Display и From утомительно. Для этого существуют два популярных крейта:

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

Крейт thiserror генерирует реализацию std::error::Error через derive-макрос. Идеален для библиотек, где нужно точное описание ошибок.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Invalid UTF-8: {0}")]
    Utf8(#[from] std::str::Utf8Error),
    #[error("Custom failure: {details}")]
    Custom { details: String },
}

// Теперь можно писать:
fn parse_data(bytes: &[u8]) -> Result<String, DataError> {
    let s = std::str::from_utf8(bytes)?; // автоматическая конвертация
    Ok(s.to_string())
}

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

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

use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))?;
    let config: Config = serde_json::from_str(&content)
        .with_context(|| "failed to parse config")?;
    Ok(config)
}

fn main() -> Result<()> {
    let config = read_config("config.json")?;
    println!("Config loaded: {:?}", config);
    Ok(())
}

Обратите внимание: anyhow::Result<T> — это псевдоним для Result<T, anyhow::Error>. Метод .with_context() добавляет человекочитаемое сообщение к ошибке, что заменяет цепочки исключений.

4. Эмуляция try/catch и обработка паник

Хотя Rust не имеет try/catch, вы можете эмулировать его с помощью замыканий и std::panic::catch_unwind. Однако это считается антипаттерном и используется редко (например, для изоляции небезопасного кода).

use std::panic;

fn risky_operation() {
    panic!("something went wrong!");
}

fn main() {
    let result = panic::catch_unwind(|| {
        risky_operation();
    });
    
    match result {
        Ok(_) => println!("No panic"),
        Err(e) => {
            let msg = if let Some(s) = e.downcast_ref::<String>() {
                s.clone()
            } else if let Some(s) = e.downcast_ref::<&str>() {
                s.to_string()
            } else {
                "unknown panic".to_string()
            };
            println!("Caught panic: {}", msg);
        }
    }
}

Лучшая альтернатива — использовать Result и оператор ? везде, где это возможно. Если вы

Блоги

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