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

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

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

Начать курс

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

В отличие от Python или Java, в Rust нет классической конструкции try/catch. Разработчики языка пошли другим путём, сделав обработку ошибок частью системы типов. Это не прихоть, а фундаментальное решение, которое позволяет гарантировать безопасность памяти и предсказуемость работы программы на этапе компиляции. В этой статье мы разберём все способы обработки ошибок в Rust: от базовых Result и Option до продвинутых методов с оператором ? и пользовательскими типами ошибок.

1. Два главных типа: Result и Option

В Rust все ошибки делятся на два лагеря: восстанавливаемые (обрабатываются через Result) и невосстанавливаемые (приводят к panic!). Также существует тип Option для случаев, когда значение может отсутствовать.

Тип Result

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

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

fn read_file_content(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

Тип Option

Option<T> — ещё один enum, который принимает значения Some(T) (значение есть) или None (значения нет). Используется, когда отсутствие данных — нормальная ситуация, а не ошибка.

fn find_user(id: u32) -> Option<String> {
    let users = vec!["Alice".to_string(), "Bob".to_string()];
    users.get(id as usize).cloned()
}

fn main() {
    match find_user(0) {
        Some(name) => println!("Найден пользователь: {}", name),
        None => println!("Пользователь не найден"),
    }
}

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

Если бы в Rust был try/catch, он выглядел бы громоздко. Вместо этого язык предлагает оператор ?. Он работает так: если результат — Ok, значение извлекается и передаётся дальше; если Err — ошибка немедленно возвращается из функции.

use std::num::ParseIntError;

fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
    let value: i32 = input.parse()?;  // если ошибка — сразу return Err
    Ok(value * 2)
}

fn main() {
    match parse_and_double("42") {
        Ok(result) => println!("Результат: {}", result),
        Err(e) => println!("Ошибка парсинга: {}", e),
    }
}

Оператор ? можно использовать только в функциях, которые возвращают Result или Option. Это вынуждает разработчика явно указывать, что функция может завершиться ошибкой — в отличие от исключений в Python, которые могут «вылететь» из любого места.

3. Когда использовать panic! и unwrap

Не все ошибки нужно обрабатывать. panic! — это макрос для невосстанавливаемых ошибок. Он останавливает программу и разворачивает стек (stack unwind). Используйте его только в исключительных ситуациях:

  • Аварийные состояния: нарушение инвариантов, которые невозможно исправить.
  • Примеры и прототипы: в учебном коде или однодневных скриптах.
  • Тесты: для проверки того, что код падает в ожидаемых местах.
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Деление на ноль! a={}, b={}", a, b);
    }
    a / b
}

unwrap() и expect() — это методы, которые вызывают panic! при ошибке. Разница: expect позволяет добавить сообщение.

let number = "42".parse::<i32>().unwrap();          // просто паника
let number = "42".parse::<i32>().expect("Не число!"); // паника с пояснением

Правило большого пальца: в production-коде используйте unwrap только если вы на 100% уверены, что ошибки не будет. Во всех остальных случаях — обрабатывайте через match или оператор ?.

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

Когда ваша программа становится сложнее, одного std::io::Error недостаточно. Rust позволяет создавать свои типы ошибок с помощью перечислений (enum) и библиотеки thiserror (для крейтов) или anyhow (для приложений).

Пример с thiserror

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Ошибка ввода-вывода: {0}")]
    Io(#[from] std::io::Error),
    #[error("Ошибка парсинга: {0}")]
    Parse(#[from] std::num::ParseIntError),
    #[error("Некорректные данные: {message}")]
    InvalidData { message: String },
}

fn process_data(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?;
    let number: i32 = content.trim().parse()?;
    if number < 0 {
        return Err(AppError::InvalidData {
            message: "Число не может быть отрицательным".into(),
        });
    }
    Ok(number)
}

Пример с anyhow

Для простых приложений, где не важна конкретная ошибка, используйте anyhow:

use anyhow::{Result, Context};

fn read_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .with_context(|| format!("Не удалось прочитать конфиг: {}", path))
}

Заключение: философия Rust в обработке ошибок

Обработка ошибок в Rust — не просто техническая деталь, а часть

Блоги

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